/* ==========================================================================
   WAVE STAFF — DESIGN SYSTEM  v5 "Ocean"
   Single source of truth for tokens + components.

   Rebased on the live resident app at wave-figma.voxturr.us — token values
   below are lifted from its stylesheet, not approximated, so the two apps
   are the same product. Deviations are only where Wave's own pairing fails
   WCAG 4.5:1 as text; each one is commented at the line.

   v5 changes vs v4
   ----------------
   • Theme     : ocean teal #0C7C8C (was violet). Header uses Wave's --ocean.
   • Radii     : Wave's rounder set — cards 20, buttons 14, sheets 28.
   • Elevation : Wave's very soft rgba(20,22,30,…) ramp.
   • Type      : Wave's denser scale — 16.5 card titles, 12.5 meta.
   • Nav       : floating rounded pill, not a flat edge-to-edge bar.
   • Icon beds : one uniform --primary-wash bed, not a per-module rainbow.
   • Easing    : Wave's cubic-bezier(.22,.61,.36,1).
   Font already matched exactly (Hanken Grotesk + Tiro Devanagari Hindi).
   ========================================================================== */

@import url('https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700;800&family=Tiro+Devanagari+Hindi:ital@0;1&display=swap');

/* --------------------------------------------------------------------------
   1. TOKENS
   -------------------------------------------------------------------------- */
:root {
  /* ---- Brand — Wave "Ocean", lifted verbatim from the resident app ------ */
  --bg:            #EAF0F1;
  --bg-top:        #F4F8F8;
  --primary:       #0C7C8C;   /* 4.91:1 on white */
  --primary-d:     #0A5F6C;   /* text tone — use on --primary-wash */
  --primary-2:     #2BA9BB;   /* highlight end of the ocean gradient */
  --primary-wash:  #E3F1F3;
  --dark:          #0A5F6C;
  --accent:        #2BA9BB;
  --sky:           #4EA8DE;   /* decorative icon accent only, never a status */
  --ic-accent:     var(--sky); /* duotone visitor-type icons (VIS_ICONS in app.js) */

  /* Aliases kept so v4 markup keeps working without a rewrite */
  --primary-50:    #E3F1F3;
  --primary-100:   #D2E8EC;
  --primary-200:   #A8D6DE;
  --primary-700:   #0A5F6C;
  --accent-50:     #E3F1F3;
  --accent-100:    #D2E8EC;

  /* ---- Semantic, raw (fills, rails, dots) — Wave's values ---- */
  --success:       #1E8A57;   /* --green */
  --warning:       #B8862B;   /* fill tone beside Wave's --amber text */
  --error:         #E0594A;   /* --coral */
  --info:          #0C7C8C;

  /* ---- Semantic, accessible pairs ---------------------------------------
     Beds are Wave's washes. Text tones are Wave's where they clear 4.5:1 and
     darkened where they do not — green 3.77:1 and coral 3.10:1 on their own
     washes, so those two are deepened for chip and inline-message text.
     The raw hexes above are untouched, so fills and rails still match Wave. */
  --success-bg:    #E4F2EA;  --success-text:  #14663F;  /* Wave --green 3.77 -> 6.06:1 */
  --warning-bg:    #F4EAD4;  --warning-text:  #8C5D11;  /* Wave --amber, 4.76:1 */
  --error-bg:      #FBE7E3;  --error-text:    #B23A2C;  /* Wave --coral 3.10 -> 4.99:1 */
  --info-bg:       #E3F1F3;  --info-text:     #0A5F6C;
  --neutral-bg:    #F2F3F6;  --neutral-text:  #474A53;  /* Wave --chip / --ink-2 */

  /* ---- Neutrals — Wave --ink / --ink-2 / --muted / --line / --line-2 ---- */
  --text:          #1B1D24;
  --text-2:        #474A53;
  --text-3:        #6B7079;
  --text-4:        #A0A4AD;   /* inactive nav, decorative only */
  --border:        #DEE0E6;
  --divider:       #ECEDF1;
  --surface:       #FFFFFF;
  --surface-2:     #F1F5F6;

  /* ---- Brand surfaces — FLAT ONLY ---------------------------------------
     No gradients anywhere in this app, by direction. The names are kept so
     existing markup resolves; every one of them is now a solid fill. */
  --grad-brand:    #0C7C8C;
  /* ONE header teal, and it is the deep one.
     Every brand header — .appbar--brand, and the heroes on profile,
     visitor-pending and service-detail — used to be --primary #0C7C8C while
     the device status bar directly above it was --dark #0A5F6C and, on
     visitor-detail, the identity band directly below it was --primary-d
     #0A5F6C too. Three stacked teal bands, the middle one a different shade
     from its neighbours on both sides: it read as a seam rather than as a
     header, which is exactly the "visually disconnected" complaint.

     Resolving to the DEEP tone rather than the mid one, for two reasons.
     The status bar and the identity band could not move — they are
     --primary-d because white sits at 7.33:1 there against 4.91:1 on
     --primary, which is what lets their small print (the 11px entry ID at
     78%, the appbar subtitle at 80%) clear AA at all. And every consumer of
     this token gains contrast by the change; none loses any. */
  --grad-header:   #0A5F6C;
  --grad-deep:     #0A5F6C;

  /* ---- Type — Wave's scale: dense, tight-tracked, heavy at the top ---- */
  --font:          'Hanken Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-hi:       'Tiro Devanagari Hindi', serif;
  --t-display:     26px;
  --t-h1:          22px;
  --t-h2:          19px;
  --t-h3:          16.5px;  /* Wave section head / card title */
  --t-body:        14.5px;  /* Wave .btn */
  --t-sm:          12.5px;  /* Wave card meta */
  --t-xs:          11px;
  --t-2xs:         10px;    /* nav labels, eyebrows */

  --lh-tight:      1.15;
  --lh-snug:       1.35;
  --lh-body:       1.5;

  /* ---- Spacing — 4pt rhythm ---- */
  --s-1: 4px;  --s-2: 8px;  --s-3: 12px; --s-4: 16px;
  --s-5: 20px; --s-6: 24px; --s-7: 28px; --s-8: 32px;
  --s-10: 40px; --s-12: 48px;

  /* ---- Radii — Wave is markedly rounder than v4 was ---- */
  --r-xs: 6px;  --r-sm: 10px; --r-md: 14px; --r-lg: 20px;
  --r-xl: 24px; --r-2xl: 28px; --r-pill: 999px;

  /* ---- Icon sizes — three, and only three ---- */
  --i-sm: 16px; --i-md: 20px; --i-lg: 24px;
  --i-stroke: 1.8;          /* Wave draws at 1.7–1.9 */

  /* ---- Elevation — Wave's ramp, verbatim ---- */
  --sh-1:   0 1px 2px rgba(20,22,30,.04);
  --sh-2:   0 3px 12px rgba(20,22,30,.05);
  --sh-3:   0 8px 24px rgba(20,22,30,.08);
  --sh-cta: 0 3px 12px rgba(20,22,30,.10);
  --sh-fab: 0 12px 28px rgba(12,124,140,.42);
  --ring:   0 0 0 3px var(--primary-wash);

  /* ---- Motion — Wave's ease-out ---- */
  --ease: cubic-bezier(.22, .61, .36, 1);
  --dur:  180ms;

  /* ---- Fixed chrome heights, used to compute scroll insets ---- */
  --h-appbar: 60px;
  --h-nav:    66px;
  --nav-gap:  14px;         /* the nav floats — insets must clear the gap too */
  --safe-b:   env(safe-area-inset-bottom, 0px);
  --safe-t:   env(safe-area-inset-top, 0px);
  --nav-inset: calc(var(--h-nav) + var(--nav-gap) + var(--safe-b));

}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}

/* --------------------------------------------------------------------------
   2. RESET
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }

/* Hide scrollbars on every axis while keeping scroll behaviour intact. */
* { scrollbar-width: none; -ms-overflow-style: none; }
*::-webkit-scrollbar { width: 0; height: 0; display: none; }

body {
  overflow-x: hidden;
  font-family: var(--font);
  font-size: var(--t-body);
  line-height: var(--lh-body);
  color: var(--text);
  background: #D9E3E4;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; border: none; background: none; }
a { color: inherit; text-decoration: none; }
:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; border-radius: var(--r-xs); }
.hi { font-family: var(--font-hi); }
svg { display: block; }

/* --------------------------------------------------------------------------
   3. DEVICE FRAME (desktop preview only — collapses to full-bleed on phones)
   -------------------------------------------------------------------------- */
.device {
  position: relative;
  width: 390px;
  height: 844px;
  margin: 24px auto;
  background: var(--bg);
  border-radius: 44px;
  border: 11px solid #14161C;
  box-shadow: 0 24px 70px rgba(20,22,30,.35);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.device::after { /* home indicator */
  content: '';
  position: absolute; bottom: 7px; left: 50%; transform: translateX(-50%);
  width: 128px; height: 5px; border-radius: var(--r-pill);
  background: rgba(20,22,30,.28); z-index: 60; pointer-events: none;
}
.statusbar {
  height: 44px; flex: none;
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 var(--s-6) 0 var(--s-5);
  font-size: var(--t-sm); font-weight: 700; letter-spacing: .2px;
  color: #fff; background: var(--dark);
}
.statusbar.on-light { color: var(--text); background: var(--bg); }
/* Preview-only device chrome — it must never compete with the app's own
   header, so it is held back to a quiet tint rather than full white. */
.statusbar__icons { display: flex; gap: 6px; align-items: center; opacity: .6; }

@media (max-width: 480px) {
  body { background: var(--bg); }
  .device {
    width: 100%; height: 100dvh; margin: 0;
    border: 0; border-radius: 0; box-shadow: none;
  }
  .device::after { display: none; }
  .statusbar { display: none; }
}

/* --------------------------------------------------------------------------
   4. APP BAR
   -------------------------------------------------------------------------- */
.appbar {
  flex: none;
  min-height: var(--h-appbar);
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  background: var(--surface);
  border-bottom: 1px solid var(--divider);
  z-index: 30;
}
.appbar--brand { background: var(--grad-header); border-bottom: none; color: #fff; }
.appbar__back {
  width: 44px; height: 44px; flex: none;   /* 44px touch target */
  display: grid; place-items: center;
  border-radius: var(--r-pill);
  margin-left: -10px;
  transition: background var(--dur) var(--ease);
}
.appbar__back:active { background: rgba(20,22,30,.06); }
.appbar--brand .appbar__back:active { background: rgba(255,255,255,.16); }
.appbar__title { font-size: var(--t-h3); font-weight: 800; letter-spacing: -.25px; line-height: var(--lh-snug); }
.appbar__sub { font-size: var(--t-xs); opacity: .8; font-weight: 600; letter-spacing: .2px; }
.appbar__actions { margin-left: auto; display: flex; gap: var(--s-1); }
.appbar__icon {
  width: 44px; height: 44px;
  display: grid; place-items: center;
  border-radius: var(--r-pill); position: relative;
  transition: background var(--dur) var(--ease);
}
.appbar__icon:active { background: rgba(20,22,30,.06); }
.appbar--brand .appbar__icon:active { background: rgba(255,255,255,.16); }
.appbar__dot {
  position: absolute; top: 9px; right: 9px;
  width: 9px; height: 9px; border-radius: var(--r-pill);
  background: var(--error); border: 2px solid var(--surface);
}
.appbar--brand .appbar__dot { border-color: var(--primary-d); }

/* --------------------------------------------------------------------------
   5. SCROLL BODY
   Content insets keep list content clear of the FAB and bottom nav.
   -------------------------------------------------------------------------- */
.screen { flex: 1; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
.screen__inner { padding: var(--s-4); }
.screen--nav   .screen__inner { padding-bottom: calc(var(--nav-inset) + var(--s-6)); }
.screen--fab   .screen__inner { padding-bottom: calc(var(--nav-inset) + 92px); }
.screen--cta   .screen__inner { padding-bottom: calc(var(--safe-b) + 104px); }

/* --------------------------------------------------------------------------
   6. TYPOGRAPHY
   Three jobs: eyebrow (context) -> title (subject) -> meta (detail).
   -------------------------------------------------------------------------- */
.display { font-size: var(--t-display); font-weight: 800; letter-spacing: -.8px; line-height: var(--lh-tight); margin: 0; }
.h1 { font-size: var(--t-h1); font-weight: 800; letter-spacing: -.5px; line-height: var(--lh-tight); margin: 0; }
.h2 { font-size: var(--t-h2); font-weight: 800; letter-spacing: -.35px; line-height: var(--lh-snug); margin: 0; }
.h3 { font-size: var(--t-h3); font-weight: 700; letter-spacing: -.2px; line-height: var(--lh-snug); margin: 0; }
.body  { font-size: var(--t-body); margin: 0; }
.small { font-size: var(--t-sm); color: var(--text-2); margin: 0; line-height: var(--lh-snug); }
.tiny  { font-size: var(--t-xs); color: var(--text-2); margin: 0; }
.muted { color: var(--text-2); }
.mono  { font-variant-numeric: tabular-nums; }
.strong { font-weight: 700; color: var(--text); }

/* Eyebrow — the smallest rung of the hierarchy, always uppercase + tracked */
.eyebrow {
  font-size: var(--t-xs); font-weight: 800;
  letter-spacing: .9px; text-transform: uppercase;
  color: var(--text-3); margin: 0;
}

.section-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-3);
  margin: var(--s-7) 0 var(--s-3);
}
.section-head:first-child { margin-top: 0; }
.section-head__title { display: flex; align-items: center; gap: var(--s-2); min-width: 0; }
/* Title + sub-line stacked, per the resident app's .sec-h .ttl — used only
   where a heading sits above descriptive copy (Quick Actions). The default
   row layout above stays untouched for headings that sit beside a count pip
   (At the gate) — flipping the base rule to column would break those. */
.section-head__title--stack { flex-direction: column; align-items: flex-start; gap: 1px; }
.section-head__sub { display: block; font-size: var(--t-sm); color: var(--text-3); font-weight: 500; margin-top: 1px; }
.section-head__link {
  flex: none;
  display: inline-flex; align-items: center; gap: 2px;
  font-size: var(--t-sm); font-weight: 700; color: var(--primary);
  padding: 6px 0 6px 8px;   /* honest tap target without shifting the baseline */
}
.section-head__link svg { width: var(--i-sm); height: var(--i-sm); }

/* Count pip that sits beside a section title */
.count-pip {
  min-width: 22px; height: 22px; padding: 0 7px;
  border-radius: var(--r-pill);
  background: var(--warning); color: #fff;
  font-size: var(--t-xs); font-weight: 800;
  display: grid; place-items: center;
  font-variant-numeric: tabular-nums;
}
.count-pip--info  { background: var(--primary); }
.count-pip--quiet { background: var(--neutral-bg); color: var(--neutral-text); }

/* --------------------------------------------------------------------------
   7. CARDS
   -------------------------------------------------------------------------- */
/* Wave cards are surface + a hairline + the softest shadow — the border is
   what separates them from the bg, the shadow only lifts them a hair. */
.card {
  background: var(--surface);
  border: 1px solid var(--divider);
  /* 16px, read off .rv-card / .dh-card in the resident app. It was
     var(--r-lg) — 20px — which is a corner sized for a full-width panel,
     not for a card 80px tall. The token still holds 20 for the things that
     want it; cards state their own. */
  border-radius: 16px;
  box-shadow: var(--sh-1);
  padding: var(--s-4);
}
/* ---- .list --------------------------------------------------------------
   This selector is used on five screens (index, visitors, service,
   service-assign) as the wrapper every card list renders into — and it had
   NO rule anywhere in this stylesheet. A plain <div> is display:block, .card
   carries no margin, so every card in every list sat flush against the next
   one, separated by nothing but two 1px borders meeting. Measured: 0.0px.

   It read as "nearly right" rather than broken because .card's shadow put a
   soft smudge where the seam was, which is exactly the kind of miss that
   survives a dozen screenshots. */
.list { display: flex; flex-direction: column; gap: var(--s-3); }

.card--flush { padding: 0; overflow: hidden; }
.card--quiet { background: var(--surface-2); box-shadow: none; border: 1px solid var(--divider); }

/* Interactive cards get a press state */
.card--tap { display: block; transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease); }
.card--tap:active { transform: scale(.985); box-shadow: var(--sh-2); }

/* Status rail = redundant, non-colour-only status coding (icon + label too) */
.card--rail { position: relative; padding-left: calc(var(--s-4) + 4px); }
.card--rail::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0;
  width: 4px; border-radius: var(--r-lg) 0 0 var(--r-lg);
  background: var(--rail, var(--border));
}
.rail-pending  { --rail: var(--warning); }
.rail-approved { --rail: var(--success); }
.rail-denied   { --rail: var(--error); }
.rail-expired  { --rail: var(--text-3); }
.rail-info     { --rail: var(--info); }

/* Card anatomy — head (identity) / meta (detail) / foot (status + id) */
.card__foot {
  display: flex; align-items: center; gap: var(--s-2);
  margin-top: var(--s-3); padding-top: var(--s-3);
  border-top: 1px solid var(--divider);
}
.card__meta {
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
  font-size: var(--t-sm); color: var(--text-2); margin-top: 3px;
  line-height: var(--lh-snug);
}
.card__meta .dot { width: 3px; height: 3px; border-radius: 50%; background: var(--border); flex: none; }

/* ---- Visitor card — the shared component rendered by visitorCard() -------
   Follows SS/Card Layout for at Gate section.png, with three corrections
   made after seeing it at real size in a list rather than as one zoomed
   mockup.

   ---- A. The foot spans the card, it does not hang off the avatar --------
   Everything used to live in one column beside the avatar: eyebrow, name,
   meta, vehicle AND the divider, status block and request id. The avatar is
   only ~48px tall, so below it a 62px-wide strip of nothing ran the whole
   rest of the card — a hard empty gutter down the left of the busiest part
   of the layout. The reference does not do this: its divider and status
   block start at the card's own left padding, not at the name's.

   So the card is two blocks stacked, not one row:
       .vcard__top   avatar | (eyebrow+time, name, meta, vehicle)
       .vcard__foot  status | rule | request id      <- full width
   The meta and vehicle stay indented under the name, which IS what the
   reference does; only the foot breaks out.

   ---- B. Smaller, but with the air kept -----------------------------------
   Every ELEMENT came down a step while the gaps between them did not:

       avatar          56 -> 48px          card padding    20 -> 16px
       name            21 -> 19px          foot margin     16 -> 13px
       eyebrow       11 -> 10.5px          status icon     20 -> 18px
       request id    16 -> 13.5px          status label  13 -> 12.5px

   Shrinking a card by closing its gaps makes it cramped; shrinking the
   elements and holding the gaps makes it smaller and still breathable, so
   the vertical rhythm inside is untouched. Measured at 390px with a vehicle
   row present, a card is 212px tall and cards sit 12px apart.

   ---- C. Hierarchy: the request id stops shouting -------------------------
   The reference sets the request id large, but it is one card blown up to
   several times screen width, where everything can afford to be big.
   Porting it literally at 16px/800 next to a 21px name made a reference
   number the second-loudest thing on the card. A guard scans this list for
   a PERSON and a STATE; the id is what they read out afterwards, once they
   have already found the row.

   So it drops to 13.5px and to --text-2, against a 19px near-black name —
   a 1.41x size gap plus a weight and colour gap, which is enough to settle
   the order without making the id hard to read aloud. (That 1.41 is a
   deliberate choice, not a measurement of the reference: the mockup is a
   flat image and its exact type scale is not recoverable from it.)

   Order of loudness, top to bottom: name > status > flat > type/org > id.
   -------------------------------------------------------------------------- */
/* text-align:left is load-bearing. A pre-approval card renders as a
   <button> so it can open its sheet in place rather than navigate, and a
   button centres its text by default — so those cards came out with the
   name, the meta run and the pre-approved line all centred while every
   <a> card beside them stayed left-aligned. The card must look identical
   whichever element it is built from. */
.vcard { padding: var(--s-4); text-align: left; }
/* Identity row: disc | the three ranked rows | the way in.

   Top-aligned, not centred. Centred against a three-row block, the disc's
   middle landed between rows two and three — so the thing that labels the
   NAME sat opposite the flat, and the card read as if it had two starting
   points. Aligned to the top it spans rows one and two, which is the pair
   it belongs to, and the eye starts in one place. The chevron re-centres on
   its own, because it belongs to the whole card rather than to any row. */
.vcard__top { display: flex; align-items: flex-start; gap: 12px; }
.vcard__b { flex: 1; min-width: 0; }

/* .vcard__go — the "way in" arrow — was here. It was a filled 30px teal
   block, then a plain chevron in a column of its own down the right, then a
   chevron at the end of the kind row. Every version cost width or attention
   to say what a tappable card already says, and the card carries a hairline
   over its band now, which reads as a card edge better than an arrow did.
   The card being one link over its whole surface is the affordance. */

/* Identity disc — the resident app's pre-approval treatment
   (reference/wave-resident-app.html:786, .va-av): a circle, white initials,
   on a dark desaturated ground set per entry.

   It was a pale teal rounded square with teal initials, which put the two
   quietest values on the card (a 14% tint and a mid-tone text) in the one
   position the eye lands on first. A dark disc gives the row an anchor and
   makes the initials legible in sun at a glance. The colour comes from
   TYPES[].av in app.js, so it doubles as a non-textual carrier of the entry
   type; white-on-disc contrast is asserted in tests/contrast.test.js. */
.vcard__av {
  position: relative; margin-top: 1px;   /* optical: sits the disc on the name's cap-height */
  width: 46px; height: 46px; flex: none; border-radius: 50%;
  display: grid; place-items: center;
  background: #776D62; color: #fff;   /* fallback; TYPES[].av overrides inline */
  font-size: 14px; font-weight: 700; letter-spacing: .2px;
}
/* The presence dot from SS/at the gate card UI.png — and from .rv-dot in the
   resident app, which is where that treatment comes from. It appears on the
   AT THE GATE queue only, and it means one specific thing: this visitor is
   standing at the barrier right now.

   Which is why it goes out when the clock does. Every entry in that list is
   at the gate by definition, so a dot that is always lit would be pure
   decoration; a timed-out entry is the one case where the visitor may well
   have given up and walked off, and the dot going dark is the card saying
   so before any text is read. */
.vcard__dot {
  position: absolute; right: -1px; bottom: -1px; z-index: 2;
  width: 12px; height: 12px; border-radius: 50%;
  background: var(--success); border: 2.5px solid var(--surface);
}

/* ---- Attribute chips ----------------------------------------------------
   The reference's Part-Time / Remote / Mid-Senior Level row, carrying what
   this arrival IS: its type, and the organisation behind it where there is
   one. Both were the two halves of the eyebrow that used to sit above the
   name — small-caps text doing the work of a label without looking like
   one. In the row built for attributes they read as what they are, and the
   name gets the top of the card to itself.

   The type keeps its own tone, matching the label on the gate card, so one
   arrival looks like itself in both lists. The organisation is neutral
   beside it: it names the arrival rather than classifying it, and two
   tinted chips side by side would read as one two-part badge. */
/* .va-badge: the arrival's type, beside the name it belongs to. 9.5px in
   the type's own tone, per the reference. The neutral second chip that
   carried the organisation is gone — the organisation is a line in the info
   stack now, behind its own type glyph, which is also what carries the
   category for anyone who cannot see the tint. */
.vtag {
  flex: none; display: inline-flex; align-items: center; gap: 5px; min-width: 0;
  padding: 3px 8px; border-radius: 7px;
  font-size: 9.5px; font-weight: 800; letter-spacing: .03em; text-transform: uppercase;
}
.vtag .t { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vtag--guest    { color: var(--success-text); background: var(--success-bg); }
.vtag--cab      { color: var(--warning-text); background: var(--warning-bg); }
.vtag--delivery { color: var(--primary-d);    background: var(--primary-wash); }

/* .vcard__time and .window were the pills in the card's old top-right slot.
   Both facts are text on the time line now. .window survives elsewhere — the
   pre-approval sheet still renders one — so only the card-scoped rules go. */
/* The name shares row 1 with the state and is the half that gives — a
   shortened name beside an intact state is readable; the reverse is not. */
.vcard__name {
  flex: 0 1 auto; min-width: 0;
  font-size: 14.5px; font-weight: 800;
  letter-spacing: -.2px; line-height: 1.25;
  color: var(--text);
}

/* ---- The two columns ---------------------------------------------------
   Identity down the left, state and time down the right. This is the change
   that does most of the work: the state sits at the same x and the same y on
   every card in the list, so a guard runs one column to find the entry that
   is still waiting instead of reading three lines of every card to get
   there. See visitorCard() for the full ranking and why it is this one. */
.vcard__r { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
.vcard__r + .vcard__r { margin-top: 5px; }

/* Row 2, left: where they were going. The flat carries weight and the card's
   darkest ink because it is the fact checked against the person at the
   barrier; the tower beside it is context and stays muted. They were the
   same 12px grey, which is what made the stack read as a wall. */
.vcard__where {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: center; gap: 5px;
  font-size: 12px; font-weight: 500; color: var(--text-2);
  line-height: var(--lh-snug);
}
.vcard__where svg { width: 13px; height: 13px; flex: none; color: var(--text-3); }
.vcard__where b { flex: none; font-weight: 800; color: var(--text); font-variant-numeric: tabular-nums; }
.vcard__sep { flex: none; color: var(--text-3); }

/* ---- The band -----------------------------------------------------------
   .va-act in the resident app: a hairline, then a row underneath holding
   what is not part of the thing itself. Ours holds the one field that VARIES
   between entries — who did this — which is what lets the block above it
   stay a fixed three rows however much an entry carries.

   It renders only on the entries that have something to put in it. A band
   holding nothing is a rule with no reason, and this card is already
   carrying a rule's worth of separation between its own rows.

   The rule is the separation the card was missing. Everything used to sit
   in one block, so a status token, a countdown, a chevron and a resident's
   own words arrived at once and none of them ranked. */
.vcard__band {
  display: flex; align-items: center; gap: 10px;
  margin-top: 11px; padding-top: 10px;
  border-top: 1px solid var(--divider);
}
/* Who did this, where that is somebody other than the guard reading the
   card: "Pre-approved by Nam Dev Sharma", "Refused by Nam Dev Sharma",
   "Approved at the gate". The quietest text on the card — it is context for
   a decision already taken, not a thing to act on — and it truncates rather
   than wraps, so a long name cannot make one card taller than its
   neighbours. Empty on an ordinary gate-raised entry, where the guard who
   raised it is the guard reading it. */
.vcard__by {
  flex: 1 1 auto; min-width: 0;
  font-size: 11.5px; font-weight: 500; color: var(--text-3);
  line-height: var(--lh-snug);
}

/* The band, right: when — and WHICH KIND of when, said by the glyph.

   Three facts land in this slot and they were all bare tabular digits:
   "00:19" is a countdown running out under a guard's hand and "08:40" is
   when somebody was logged this morning, and nothing separated them. The
   glyph does: an hourglass for time REMAINING, a clock for anything that is
   a time of day — a timestamp, or an arrival window, which announces itself
   as a range by being one. It reads in greyscale, which the countdown's
   amber-to-red never did on its own.

   Tabular, so the values stack in a column under the states, and 700 rather
   than 500: it outranks the kind row below without reaching the flat beside
   it. */
.vcard__when {
  flex: none; margin-left: auto;
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 12px; font-weight: 700; color: var(--text-2);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.vcard__when svg { width: 13px; height: 13px; flex: none; color: var(--text-3); }
/* The countdown's glyph escalates with it — an hourglass that stays grey
   while its digits go red would be reporting a different urgency from the
   number beside it. */
.vcard__left svg { color: currentColor; }

/* Row 3: what kind of arrival. The quietest row on the card, and the only
   one that can be missing a value — an unbranded guest is the badge alone.

   The badge leads it rather than trailing the name. After a name it landed
   wherever that name happened to end, so a list put badges at as many x
   positions as it had cards; first on its own row, they form a column and a
   guard can run the types without reading the names. */
.vcard__kind {
  display: flex; align-items: center; gap: 7px; min-width: 0;
  margin-top: 7px;
  font-size: 12px; font-weight: 500; color: var(--text-2);
}
.vcard__kind .mono { color: var(--text-3); }

/* The state, at the right edge of the name's row — a TOKEN, not a run of
   text. It is the second most valuable thing on the card and it was set at
   the same size, weight and colour intensity as the countdown underneath
   it, so the right column read as two equal values rather than as a status
   and its clock. A tinted bed settles that in one move.

   (This card carried a status pill before, and it was removed for a good
   reason: it sat beside a countdown that was filled and amber too, so on a
   pending entry the two read as one two-part badge. That reason is gone —
   the countdown is quiet text now, and stays quiet until it is genuinely
   running out — so the bed comes back to the half that earns it.)

   The dot every status shared is now each status's OWN glyph: a tick for
   approved, a cross for denied, a clock for one still with the resident, an
   alert for one that ran out. Statuses differ by SHAPE now, not only by
   tone, which is what lets this column be scanned in greyscale or by a
   guard who cannot separate amber from coral. */
.vcard__state {
  flex: none; margin-left: auto;
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 8px; border-radius: 7px;
  font-size: 11.5px; font-weight: 800; white-space: nowrap;
}
.vcard__state svg { width: 12px; height: 12px; flex: none; }
.vcard__state--wait { color: var(--warning-text); background: var(--warning-bg); }
.vcard__state--ok   { color: var(--success-text); background: var(--success-bg); }
.vcard__state--no   { color: var(--error-text);   background: var(--error-bg); }
.vcard__state--info { color: var(--info-text);    background: var(--info-bg); }
.vcard__state--idle { color: var(--neutral-text); background: var(--neutral-bg); }

/* The countdown, on the register only — and it escalates in three steps
   rather than starting alarmed.

   It used to be amber from the moment it appeared, which put a warning
   colour on an entry with two minutes still to run and made the clock read
   louder than the STATE above it. That is the hierarchy upside down: on
   this screen the state is what a guard scans for and the clock qualifies
   it. Quiet until the last minute, amber inside it, red inside thirty
   seconds — so colour here means "now", rather than meaning "pending".

   The gate card's clock keeps its amber-from-the-start pill, deliberately:
   every row in that list is a decision waiting to be taken against exactly
   this clock, where here most rows are already settled. Different jobs,
   different alarm thresholds. */
.vcard__left { font-weight: 800; font-variant-numeric: tabular-nums; }
.vcard__left.is-soon   { color: var(--warning-text); }
.vcard__left.is-urgent { color: var(--error-text); }

/* The flat, lifted out of the meta run into its own chip — it is the fact a
   guard matches against what the visitor claims. */
/* .vcard__meta, .vcard__flat and the flat chip's glyph lived here. The card
   is the resident app's Pre-Approval composition now: the flat and its tower
   are one muted line in the info stack, behind a door glyph, rather than a
   chip and a dot and a tower. .vcard__line carries them. */

/* .vcard__veh went the same way: a plate is another line in the stack, with
   the car glyph doing what the class used to. */

/* .vcard__by's old rules (a green "Pre-approved by" line inside the card
   body) and .vcard__note / --no (the resident's refusal quoted in red, and
   the guard's own note in body text) lived here.

   The note is off the card: prose of unbounded length in a list built for
   glancing, and the one element that could push a card to any height. The
   refusal is off it too — quoted in full and in red it was the loudest
   thing on any card that carried it, and what a guard needs at list level
   is that the entry WAS refused and by whom, which the band now says in
   four words. Both are on the entry's own screen, whole.

   .vcard__by is still a class, but it belongs to the band now and is
   defined up there with it. */


/* The foot went with the composition. .vcard__foot, .vcard__state's pill,
   .vcard__stb / __st / __ss and the --solo variant all belonged to a status
   BLOCK sitting under a rule: an icon, a bold label and a plain-language
   second line, roughly 46px of card spent saying "Awaiting resident · Not
   answered yet" on every entry in a list where every entry was already
   saying who and when.

   The Pre-Approval card in the resident app has no such block. It puts the
   state inline on the time line as ONE WORD in its tone — "Inside",
   "Expected", "Rejected" — and that is what .vcard__state is now, further
   up this file. The long labels have not been deleted from the app: STATUS
   still carries label and sub, and the entry's own screen still shows them,
   because a screen with nothing but a status on it can afford the sentence
   that a card in a list cannot. */

/* ---- At the gate: the queue row ----------------------------------------
   The composition is SS/At the gate.png, in this app's own tokens: a disc,
   the name with the arrival's label beside it, a flat chip and a countdown
   chip under them, then a hairline and the two decisions.

   It is still a ROW inside one shared card rather than a card of its own —
   .gate-req in the resident app (reference:957) — because five cards' worth
   of padding and shadow is where the height of this list used to go.

   Six fields, and only six: disc, name, label, flat, clock, decision. They
   rank by SIZE and by LINE — a 15px name over 12px chips — and the two
   tinted chips never share a line, so the label's tone reads as a category
   and the clock's as an alarm rather than as a matched pair.

   The widths are the whole problem here. This row is about 300px wide on a
   phone once the shell, the card and the buttons have taken their share, so
   every element states which of them gives first:

     the NAME      shrinks and ellipsises — it is the one field the initials
                   disc beside it already half-answers, and the full form is
                   one tap down the row's own link;
     the FLAT      shrinks only after the name has, and only in its text;
     the CLOCK     never shrinks. It was the last item on a three-item line
                   before this pass and so it was the one that got clipped
                   off the screen — the single thing on the row that is
                   counting down, silently cut. flex:none, permanently. */
/* ONE CARD PER ENTRY. These were rows sharing a card, divided by a
   hairline — five cards' worth of padding and shadow collapsed into one,
   which is where the height of this list used to go. The owner asked for
   each arrival to read as its own thing, and the reference is a single
   standing card, so the saving is spent back deliberately: a card, a 12px
   gap from .list, and no hairline. A hairline says "next line of the same
   thing"; a gap says "next person".

   The padding is 10px at the sides rather than the card default's 16. This
   card is a horizontal strip about 330px wide carrying six fields — the
   margin the default buys elsewhere is width the name would pay for here,
   and the reference's own padding is about the same proportion. */
.gaterow {
  display: flex; align-items: center; gap: 9px;
  padding: 11px 10px;
}
/* The link is the identity block only — see gateRow() for why. */
.gaterow__hit {
  flex: 1; min-width: 0; display: flex; align-items: center; gap: 9px;
  transition: transform var(--dur) var(--ease);
}
.gaterow__hit:active { transform: scale(.985); }
/* 46px — .rv-av in the resident app, and the same disc .vcard__av and
   .dsheet__av carry. It was 38px, taken from that app's gate ROW rather
   than from its card; the entries here are cards now, and a guard moving
   between Home and the register was seeing the same visitor drawn at two
   sizes. One person, one disc, on every screen. It costs this card eight
   pixels of text width, which the label's cap below is measured against.

   Tinted by TYPE, so one visitor is also one colour everywhere. */
.gaterow__av {
  width: 46px; height: 46px; flex: none; border-radius: 50%;
  display: grid; place-items: center;
  background: #776D62; color: #fff;
  font-size: 14px; font-weight: 700; letter-spacing: .2px;
}
.gaterow__b { flex: 1; min-width: 0; }

/* Line 1 — WHO, and WHAT they are. The label sits directly against the name
   rather than pinned to the right edge: that is the reference's own
   arrangement, and it reads as one phrase — "Sandeep Yadav, Rapido" — where
   a right-pinned tag reads as a separate column of metadata. */
.gaterow__top { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.gaterow__n {
  flex: 0 1 auto; min-width: 0;
  font-size: 14.5px; font-weight: 800; letter-spacing: -.2px; color: var(--text);
}
/* The arrival's own name — RAPIDO, BLINKIT, GUEST — in the type's tone.
   Small, uppercase and tinted, so it reads as a category beside the name
   rather than as a second name.

   max-width in CH is what protects the name, and both the unit and the
   number are load-bearing. A flex item clamped by max-width FREEZES, so the
   shrink a long brand would otherwise force onto the name lands on the
   brand instead — which is the whole trick.

   The number is measured, not chosen, and it is re-measured whenever the
   card's geometry moves: the line has about 161px once the 46px disc and
   the two buttons have taken theirs, a 13-character name ("Sandeep Yadav")
   needs 98 of it, the gap takes 6, and what is left is 57 — 11ch at this
   size. So a full name and any brand fit together, GUEST / RAPIDO / BLINKIT
   clear the cap whole, and only a longer brand ellipsises inside its own
   pill. A short brand beside a long NAME leaves the name free to shrink
   instead, which is the right way round. */
.gaterow__tag {
  flex: 0 1 auto; min-width: 0; max-width: 11ch;
  padding: 3px 6px; border-radius: 6px;
  font-size: 10px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase;
}
.gaterow__tag .t { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gaterow__tag--guest    { color: var(--success-text); background: var(--success-bg); }
.gaterow__tag--cab      { color: var(--warning-text); background: var(--warning-bg); }
.gaterow__tag--delivery { color: var(--primary-d);    background: var(--primary-wash); }

/* Line 2 — WHERE they are going, and HOW LONG the resident has left. Two
   chips of the same height and radius, per the reference: the pair reads as
   one strip, and only one of them carries a live value. */
.gaterow__s { display: flex; align-items: center; gap: 6px; margin-top: 6px; min-width: 0; }
/* Near-black, not muted: the flat is a fact being checked against what the
   person at the barrier says, not a caption — so flex:none, and it never
   ellipsises. "C-11…" is not a flat number. The door glyph tells a guard at
   a glance that this chip is a FLAT and the one beside it is a clock,
   without either having to be read, and it is the only part of the chip
   that a timed-out row gives up — see gateRow(). */
.gaterow__flat {
  flex: none; min-width: 0;
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 8px; border-radius: 8px;
  background: var(--surface-2); border: 1px solid var(--divider);
  font-size: 12px; font-weight: 800; color: var(--text); letter-spacing: -.1px;
  font-variant-numeric: tabular-nums;
}
.gaterow__flat svg { width: 12px; height: 12px; flex: none; color: var(--text-3); }
.gaterow__flat .t { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The countdown keeps .timer's own escalation — amber, then red, then red
   with the word — and is reshaped to the flat chip's height and radius so
   the two sit as a pair. It never shrinks: see the note at the top. */
.gaterow__s .timer {
  font-size: 12px; padding: 4px 8px; border-radius: 8px;
}
.gaterow__s .timer svg { width: 12px; height: 12px; }
/* The countdown is the row's live element, so it keeps .timer's own
   escalation rather than being restyled down to fit. */
.gaterow__s .timer { font-size: 11.5px; padding: 3px 8px; }
.gaterow__s .timer svg { width: 12px; height: 12px; }
/* The decision pair, fenced off from the identity block by a hairline —
   the reference's own divider. It earns its pixel: the row is a LINK on the
   left and two CONTROLS on the right, and nothing else on the row says
   where one stops and the other starts. */
.gaterow__acts { display: flex; align-items: center; gap: 6px; flex: none; }
.gaterow__acts::before {
  content: ''; flex: none;
  width: 1px; height: 34px; margin-right: 0;
  background: var(--divider);
}
.gaterow__act {
  width: 38px; height: 38px; flex: none; border-radius: 50%;
  border: none; display: grid; place-items: center; cursor: pointer;
  transition: transform var(--dur) var(--ease);
}
.gaterow__act:active { transform: scale(.9); }
.gaterow__act svg { width: 17px; height: 17px; }
/* Deny is available but never inviting: a bordered neutral disc carrying
   the refusal tone on the glyph alone. Approve is the affirmative one and
   takes the filled green — exactly the .gm.decline / .gm.approve pairing in
   the resident app. Weight says which is which before colour does, and the
   two glyph SHAPES (a cross, a tick) say it again, so the pair still reads
   for a guard with a red-green deficiency. */
.gaterow__act--no {
  background: var(--surface); color: var(--error-text);
  border: 1px solid var(--border);
}
.gaterow__act--no:active { background: var(--error-bg); }
.gaterow__act--yes {
  background: var(--success); color: #fff;
  box-shadow: 0 3px 10px rgba(30,138,87,.32);
}

/* Narrow phones: the row gives up GEOMETRY, never a field. The disc and the
   two buttons lose 4px each and every gap closes, which buys the name about
   eighteen — and the name is the only element here that truncates, so that
   is where the room has to go.

   The alternative was dropping the flat onto the second line to give the
   name the full width. That trades one truncation for a worse one: line 2
   would then hold flat + label + clock in the same space, and the label —
   the field that says Rapido or Blinkit — is what would disappear. A
   shortened name beside an intact flat is still checkable; a row with no
   label is not. The buttons stay at 34px, which is still a thumb target at
   the edge of the screen where the thumb already is. */
@media (max-width: 360px) {
  .gaterow { padding-left: var(--s-2); padding-right: var(--s-2); }
  .gaterow, .gaterow__hit { gap: 9px; }
  .gaterow__av { width: 42px; height: 42px; font-size: 13px; }
  .sk--avatar-sm { width: 42px; height: 42px; }
  .gaterow__top { gap: 6px; }
  .gaterow__flat { padding-left: 6px; padding-right: 6px; }
  .gaterow__acts { gap: 6px; }
  .gaterow__acts::before { margin-right: 0; }
  .gaterow__act { width: 34px; height: 34px; }
  .gaterow__act svg { width: 16px; height: 16px; }
}

/* The request id's rules lived here — a two-line labelled column, then one
   line with a chevron. Nothing on the card has a slot for an id: it is a
   number read out AFTER deciding, not one this list is scanned by, so it
   went to the entry's own screen. Nothing renders these, so they are gone
   rather than left to rot. */

/* Narrow phones: the status run is the half that gives. Its second line
   already ellipsises, so the run simply gets shorter rather than the two
   halves stacking — which is what used to happen, and which cost a whole
   row. The padding this block used to trim went with the state's bed. */
@media (max-width: 360px) {
  .vcard__state { gap: 5px; }
  .vcard__foot { gap: 8px; }
  .vcard { padding: var(--s-3); }
}

/* --------------------------------------------------------------------------
   8. AVATAR
   -------------------------------------------------------------------------- */
.avatar {
  width: 46px; height: 46px; flex: none;
  border-radius: var(--r-md);
  display: grid; place-items: center;
  font-weight: 800; font-size: var(--t-h3);
  background: var(--primary-wash); color: var(--primary-d);
  letter-spacing: -.4px;
}
.avatar--sm { width: 34px; height: 34px; font-size: var(--t-sm); border-radius: var(--r-sm); }
.avatar--lg { width: 56px; height: 56px; font-size: var(--t-h2); border-radius: var(--r-lg); }
.avatar--brand { background: var(--grad-brand); color: #fff; }

/* --------------------------------------------------------------------------
   9. CHIPS / BADGES
   Every status carries an icon + text, never colour alone.
   -------------------------------------------------------------------------- */
.chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 10px; border-radius: var(--r-pill);
  font-size: var(--t-xs); font-weight: 800;
  letter-spacing: .2px; white-space: nowrap;
  background: var(--neutral-bg); color: var(--neutral-text);
}
.chip svg { width: 13px; height: 13px; flex: none; }
.chip--success { background: var(--success-bg); color: var(--success-text); }
.chip--warning { background: var(--warning-bg); color: var(--warning-text); }
.chip--error   { background: var(--error-bg);   color: var(--error-text); }
.chip--info    { background: var(--info-bg);    color: var(--info-text); }
.chip--outline { background: transparent; border: 1px solid var(--border); color: var(--text-2); }

/* Filter chips */
.filters {
  display: flex; gap: var(--s-2); overflow-x: auto;
  padding: 0 var(--s-4) var(--s-3);
  scrollbar-width: none; -ms-overflow-style: none;
}
.filters::-webkit-scrollbar { display: none; }
.filter {
  flex: none; min-height: 38px; padding: 8px 14px;
  border-radius: var(--r-pill);
  background: var(--surface); border: 1px solid var(--border);
  font-size: var(--t-sm); font-weight: 700; color: var(--text-2);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.filter[aria-pressed="true"] { background: var(--primary); border-color: var(--primary); color: #fff; }
.filter__count {
  display: inline-block; margin-left: 5px;
  padding: 0 6px; border-radius: var(--r-pill);
  background: rgba(20,22,30,.06); font-size: var(--t-xs);
  font-variant-numeric: tabular-nums;
}
.filter[aria-pressed="true"] .filter__count { background: rgba(255,255,255,.24); }

/* --------------------------------------------------------------------------
   10. TABS / SEGMENTED CONTROL
   -------------------------------------------------------------------------- */
/* The strip had a --primary-50 bed (#E3F1F3) sitting on the page's --bg
   (#EAF0F1). Three points apart on every channel: on a phone in daylight
   that is not a container, it is a smudge, and the unselected tab had no
   visible bed at all while the selected one read as a lone white pill
   floating next to a ghost.

   Two changes fix it. The strip becomes a white card with a real border, so
   it separates from the page the way every other surface in this app does.
   And selection stops being "lifts to white" and becomes solid --primary
   with white type — which is exactly what the .filter chips directly below
   it already do. Two selection languages on one screen, six inches apart,
   was the deeper problem: a guard had to learn that a white pill means
   chosen up here and a teal pill means chosen down there. */
.tabs {
  display: flex; gap: var(--s-1); padding: 4px;
  margin: var(--s-4) var(--s-4) var(--s-3);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-md); box-shadow: var(--sh-1);
}
.tab {
  flex: 1; min-height: 42px; padding: 9px;
  border-radius: var(--r-sm);
  font-size: var(--t-sm); font-weight: 700; color: var(--text-2);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.tab:active { background: var(--surface-2); }
/* #fff on --primary is 4.91:1 — AA at this size and weight. */
.tab[aria-selected="true"] { background: var(--primary); box-shadow: var(--sh-1); color: #fff; }
.tab[aria-selected="true"]:active { background: var(--primary-700); }

/* Inline segmented control, used inside forms (Pickup / Drop, etc.) */
.segment {
  display: flex; gap: var(--s-1); padding: 4px;
  background: var(--surface-2); border: 1px solid var(--divider);
  border-radius: var(--r-md);
}
.segment__btn {
  flex: 1; min-height: 44px; border-radius: var(--r-sm);
  font-size: var(--t-sm); font-weight: 700; color: var(--text-2);
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.segment__btn svg { width: var(--i-sm); height: var(--i-sm); }
.segment__btn[aria-pressed="true"] { background: var(--surface); color: var(--primary); box-shadow: var(--sh-1); }

/* --------------------------------------------------------------------------
   11. SEARCH
   -------------------------------------------------------------------------- */
.search { position: relative; margin: 0 var(--s-4) var(--s-3); }
.search__input {
  width: 100%; min-height: 48px;
  padding: 12px 44px 12px 44px;
  border-radius: var(--r-md);
  border: 1px solid var(--border); background: var(--surface);
  font-size: var(--t-body);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.search__input::placeholder { color: var(--text-3); }
.search__input:focus { outline: none; border-color: var(--primary); box-shadow: var(--ring); }
.search__icon {
  position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
  color: var(--text-3); pointer-events: none;
}
.search__clear {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  width: 40px; height: 40px; display: none; place-items: center;
  color: var(--text-2); border-radius: var(--r-pill);
}
.search__input:not(:placeholder-shown) ~ .search__clear { display: grid; }

/* --------------------------------------------------------------------------
   12. BUTTONS — four tiers, one primary per screen
   -------------------------------------------------------------------------
   primary    filled violet    the single most likely next action
   secondary  outlined violet  the reversible alternative beside it
   tertiary   tinted violet    in-card / repeated actions
   quiet      text only        low-stakes, dismissive
   -------------------------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  min-height: 48px; padding: 0 var(--s-5);
  border-radius: var(--r-md);
  font-size: var(--t-body); font-weight: 700; letter-spacing: -.1px;
  transition: transform var(--dur) var(--ease), background var(--dur) var(--ease), opacity var(--dur) var(--ease);
  -webkit-tap-highlight-color: transparent;
}
.btn:active { transform: scale(.98); }      /* no layout-bound shift */
.btn svg { width: var(--i-md); height: var(--i-md); flex: none; }

.btn--primary   { background: var(--primary); color: #fff; box-shadow: var(--sh-cta); }
.btn--primary:active { background: var(--primary-700); }
.btn--secondary { background: var(--surface); color: var(--primary); border: 1.5px solid var(--primary-200); }
.btn--secondary:active { background: var(--primary-50); }
.btn--tertiary  { background: var(--primary-50); color: var(--primary-d); }
.btn--tertiary:active { background: var(--primary-100); }
.btn--quiet     { background: transparent; color: var(--text-2); padding: 0 var(--s-3); }
.btn--quiet:active { background: var(--neutral-bg); }

.btn--grad    { background: var(--grad-brand); color: #fff; box-shadow: var(--sh-cta); }
.btn--danger  { background: var(--error-bg); color: var(--error-text); }
.btn--success { background: var(--success); color: #fff; }

/* Back-compat aliases — older markup keeps working */
.btn--outline { background: var(--surface); color: var(--primary); border: 1.5px solid var(--primary-200); }
.btn--ghost   { background: var(--primary-50); color: var(--primary-d); }
.btn--accent  { background: var(--accent); color: #fff; }

.btn--block   { display: flex; width: 100%; }
.btn--sm      { min-height: 40px; padding: 0 var(--s-4); font-size: var(--t-sm); border-radius: var(--r-sm); }
.btn--sm svg  { width: var(--i-sm); height: var(--i-sm); }
.btn--lg      { min-height: 54px; font-size: var(--t-h3); border-radius: var(--r-md); }
.btn[disabled], .btn[aria-disabled="true"] { opacity: .42; pointer-events: none; }

.btn--loading { pointer-events: none; position: relative; color: transparent !important; }
.btn--loading::after {
  content: ''; position: absolute; width: 20px; height: 20px;
  border: 2.5px solid rgba(255,255,255,.35); border-top-color: #fff;
  border-radius: var(--r-pill); animation: spin .7s linear infinite;
}
.btn--secondary.btn--loading::after,
.btn--outline.btn--loading::after,
.btn--tertiary.btn--loading::after,
.btn--ghost.btn--loading::after { border-color: var(--primary-100); border-top-color: var(--primary-d); }
@keyframes spin { to { transform: rotate(360deg); } }

/* Sticky bottom CTA — respects safe area */
.cta-bar {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 40;
  padding: var(--s-3) var(--s-4) calc(var(--s-3) + var(--safe-b));
  background: rgba(255,255,255,.94);
  backdrop-filter: blur(14px);
  border-top: 1px solid var(--divider);
  display: flex; align-items: center; gap: var(--s-3);
}

/* FAB — sits above the nav bar, never over content (content inset handles it) */
.fab {
  position: absolute; right: var(--s-4); z-index: 45;
  /* s-1, not s-3. A FAB floating over a scrolling list is expected — that is
     what it is for — but on Home it was resting 19px into the bottom card of
     the quick-action grid at the top of the scroll, clipping that module's
     subtitle before the guard had touched anything. Dropping it 8px, with
     the grid re-derived above, clears the card at rest; it still floats over
     the At-the-gate list below, which is the behaviour that was wanted. */
  bottom: calc(var(--nav-inset) + var(--s-1));
  min-height: 52px; padding: 0 var(--s-5);
  display: inline-flex; align-items: center; gap: var(--s-2);
  border-radius: var(--r-pill);
  background: var(--grad-brand); color: #fff;
  font-weight: 700; font-size: var(--t-body);
  box-shadow: var(--sh-fab);
  transition: transform var(--dur) var(--ease);
}
.fab:active { transform: scale(.96); }

/* --------------------------------------------------------------------------
   13. FORMS — label above, inline validation, 52px controls
   -------------------------------------------------------------------------- */
.field { margin-bottom: var(--s-4); }
.field__label {
  display: block; margin-bottom: 7px;
  font-size: var(--t-sm); font-weight: 700; color: var(--text);
  letter-spacing: -.05px;
}
.field__req { color: var(--error-text); }
.field__opt { font-weight: 600; color: var(--text-3); }
.field__control {
  width: 100%; min-height: 52px;
  padding: 14px var(--s-4);
  border-radius: var(--r-md);
  border: 1.5px solid var(--border); background: var(--surface);
  font-size: var(--t-body);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.field__control::placeholder { color: var(--text-3); }
.field__control:focus { outline: none; border-color: var(--primary); box-shadow: var(--ring); }
textarea.field__control { min-height: 88px; resize: none; line-height: var(--lh-body); }
select.field__control {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%2364708B' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 14px center;
  padding-right: 44px;
}
.field__hint { margin-top: 6px; font-size: var(--t-sm); color: var(--text-2); line-height: var(--lh-snug); }
.field__error {
  display: none; align-items: center; gap: 5px;
  margin-top: 6px; font-size: var(--t-sm); font-weight: 700; color: var(--error-text);
}
.field__error svg { width: 14px; height: 14px; flex: none; }
.field.is-invalid .field__control { border-color: var(--error); box-shadow: 0 0 0 3px var(--error-bg); }
.field.is-invalid .field__error { display: flex; }
.field.is-valid .field__control { border-color: var(--success); }

/* Grouped fields — a titled block so long forms read as sections, not a wall */
.fieldset { margin-bottom: var(--s-5); }
.fieldset__head { display: flex; align-items: center; gap: var(--s-2); margin-bottom: var(--s-3); }
.fieldset__head .eyebrow { flex: none; }
.fieldset__head::after { content: ''; flex: 1; height: 1px; background: var(--divider); }

/* Stepper */
.stepper { display: flex; align-items: center; gap: var(--s-3); }
.stepper__btn {
  width: 48px; height: 48px; flex: none;
  border-radius: var(--r-md); border: 1.5px solid var(--border);
  background: var(--surface); color: var(--primary);
  display: grid; place-items: center; font-size: 22px; font-weight: 700;
}
.stepper__btn:active { background: var(--primary-50); }
.stepper__val { flex: 1; text-align: center; font-size: var(--t-h2); font-weight: 800; }

/* Photo capture */
.photo {
  width: 100%; min-height: 128px;
  border-radius: var(--r-md);
  border: 2px dashed var(--border); background: var(--surface);
  display: grid; place-items: center; gap: 6px;
  color: var(--text-2); font-size: var(--t-sm); font-weight: 700;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.photo:active { border-color: var(--accent); background: var(--accent-50); }
.photo svg { width: 28px; height: 28px; color: var(--primary-d); }
.photo--filled { border-style: solid; border-color: var(--success); padding: 0; overflow: hidden; }

/* Read-only summary row — "you already chose this, here's how to change it" */
.picked {
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  background: var(--primary-50); border: 1px solid var(--primary-100);
  border-radius: var(--r-md); margin-bottom: var(--s-4);
}
.picked__body { flex: 1; min-width: 0; }
.picked__k { display: block; font-size: var(--t-xs); font-weight: 800; letter-spacing: .7px;
             text-transform: uppercase; color: var(--primary-700); opacity: .75; }
.picked__v { display: block; font-size: var(--t-body); font-weight: 700; color: var(--text); margin-top: 1px; }
.picked__action { flex: none; font-size: var(--t-sm); font-weight: 800; color: var(--primary); padding: 8px; margin: -8px; }

/* --------------------------------------------------------------------------
   14. STEP INDICATOR
   -------------------------------------------------------------------------- */
.steps { display: flex; align-items: center; gap: 6px; padding: 0 var(--s-4) var(--s-3); }
.steps__item { flex: 1; height: 4px; border-radius: var(--r-pill); background: var(--divider); transition: background var(--dur) var(--ease); }
.steps__item.is-done   { background: var(--primary-200); }
.steps__item.is-active { background: var(--primary); }

/* --------------------------------------------------------------------------
   15. BOTTOM NAV (3 destinations — Home, Visitors, Profile)
   -------------------------------------------------------------------------- */
/* Copied wholesale from the resident app's own nav per the designer's direct
   instruction to "totally copy the style" (reference/wave-resident-app.html
   CSS at :708-712, markup at :4282-4286 — a 4-item .ni set there, trimmed to
   our 3: Home, Visitors, Profile).

   .navwrap's fade is a deliberate, noted deviation from this file's "no
   gradients anywhere" direction (section 1): a direct instruction to copy a
   specific style outranks a standing convention, so it is implemented as
   asked rather than reworked into another flat-fill approximation. It
   replaces the .nav::before opaque-tray workaround this file used
   previously to fake the same effect without a gradient. Kept out of the
   Auth CSS section on purpose — the auth-splash test caps that section at 2
   gradients, and this rule lives here, in section 15, well before Auth
   starts further down the file. */
.navwrap {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 40;
  /* Resident app: `padding: 0 14px 14px`. --nav-gap is also 14px here, so
     the horizontal/top numbers match exactly; the bottom side additionally
     clears env(safe-area-inset-bottom) so the pill still floats clear of a
     notched phone's home indicator, which the resident app's own layout
     doesn't need to account for. */
  padding: 0 var(--nav-gap) calc(var(--nav-gap) + var(--safe-b));
  background: linear-gradient(to top, var(--bg) 58%, transparent);
}
.nav {
  height: var(--h-nav);
  display: grid; grid-template-columns: repeat(3, 1fr); align-items: center;
  padding: 0 4px;
  background: rgba(255,255,255,.94);
  backdrop-filter: blur(14px) saturate(1.4);
  -webkit-backdrop-filter: blur(14px) saturate(1.4);
  border: 1px solid var(--border);
  border-radius: 18px;
  box-shadow: var(--sh-3);
}
.nav__item {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 4px;
  color: var(--text-4); font-size: var(--t-2xs); font-weight: 600;
  position: relative;
  transition: color var(--dur) var(--ease);
}
.nav__item svg { width: 23px; height: 23px; } /* resident app renders nav icons at 23px, not the standard --i-lg (24px) */

/* ---- Duotone active state ------------------------------------------------
   The resident app's own mechanism (.ic-a / svg.ic at reference:706): the
   glyph is two layers, an outline in --ic-stroke and one filled shape in
   --ic-accent, and the tab strip sets both.

   Inactive is unchanged from what this app already drew — --ic-accent goes
   transparent, leaving the same single-weight outline in the same grey. All
   of the difference is loaded onto the ACTIVE tab, which is the one that
   was not announcing itself: at 23px, a line that changes from grey to teal
   is not a difference you catch without looking for it. A filled shape
   appearing inside the glyph is.

   Tones are this app's, not the reference's: it pairs near-black with a sky
   blue that has no other job in that product. Here the fill is the brand
   teal, which is already what the indicator pill and the label are using,
   so the active tab reads as one object in one colour rather than as a
   glyph borrowing an accent from somewhere else.

   svg.ic rather than .nav__item svg, so the icon's outline is coloured
   independently of the LABEL under it — the label stays --primary-d while
   the outline goes darker, exactly as the reference has it. */
.nav__item { --ic-stroke: var(--text-4); --ic-accent: transparent; }
.nav__item[aria-current="page"] { --ic-stroke: var(--text); --ic-accent: var(--primary); }
svg.ic { color: var(--ic-stroke); }
.ic-a { fill: var(--ic-accent); transition: fill var(--dur) var(--ease); }

.nav__item[aria-current="page"] { color: var(--primary-d); font-weight: 700; }
/* Active indicator — the signature of this nav treatment: a 22x3px pill
   above the active item, not colour alone. */
.nav__item[aria-current="page"]::before {
  content: ''; position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  width: 22px; height: 3px; border-radius: var(--r-pill);
  background: var(--primary);
}
.nav__badge {
  position: absolute; top: 4px; left: 50%; margin-left: 6px;
  min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: var(--r-pill); background: var(--error); color: #fff;
  font-size: 10px; font-weight: 800; line-height: 18px; text-align: center;
  border: 1.5px solid var(--surface); font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   16. SKELETON LOADERS
   -------------------------------------------------------------------------- */
/* Solid + opacity pulse. The usual shimmer is a moving gradient, which this
   app does not allow, so the skeleton breathes instead of sweeping. */
.sk { background: #E4E9EE; animation: pulse-sk 1.3s ease-in-out infinite; border-radius: var(--r-sm); }
@keyframes pulse-sk { 0%, 100% { opacity: 1; } 50% { opacity: .5; } }
/* Matches .vcard__av exactly (48px / 14px radius). A skeleton that is a
   different size from the thing it stands in for makes the list jump when
   the real cards land. */
.sk--avatar { width: 46px; height: 46px; border-radius: 50%; flex: none; }
/* Matches .gaterow__av, so Home's queue does not resize when the
   placeholder is swapped for the real rows. */
.sk--avatar-sm { width: 46px; height: 46px; }
.sk--line { height: 12px; }
.sk--title { height: 15px; width: 55%; }
.sk--chip { height: 22px; width: 78px; border-radius: var(--r-pill); }

/* --------------------------------------------------------------------------
   17. EMPTY STATE
   -------------------------------------------------------------------------- */
.empty { text-align: center; padding: var(--s-10) var(--s-6); }
/* The art was an 88px --primary-50 bed on the page's --bg. Those two
   tokens compute to 1.00:1 against each other — not a low-contrast
   container, LITERALLY the same luminance — so the disc did not exist and
   the glyph inside it floated on the page as a thin teal outline. An empty
   state that reads as a faded, inactive element is worse than none: it
   looks like a list that failed to load rather than an answer.

   Solid ground, white glyph. --primary-d puts the disc 6.36:1 against the
   page and the glyph 7.33:1 against the disc, which is the same move the
   .tabs strip made when it stopped being a --primary-50 smudge. */
.empty__art {
  width: 88px; height: 88px; margin: 0 auto var(--s-4);
  border-radius: var(--r-2xl);
  background: var(--primary-d); color: #fff;
  display: grid; place-items: center;
  box-shadow: 0 8px 20px rgba(10,95,108,.22);
}
/* Good news, not merely absence. "No denials today" and "nobody is waiting"
   are outcomes a guard wants to see, so they get the success ground rather
   than the brand one — the same green the approved status block uses, at
   its text weight (7.00:1 under white) because this is a filled disc and
   not a tinted bed. */
.empty__art--ok { background: var(--success-text); box-shadow: 0 8px 20px rgba(20,102,63,.22); }
.empty__art svg { width: 40px; height: 40px; }
.empty__title { font-size: var(--t-h3); font-weight: 800; margin: 0 0 6px; }
.empty__text { font-size: var(--t-sm); color: var(--text-2); margin: 0 0 var(--s-5); line-height: var(--lh-body); }

/* --------------------------------------------------------------------------
   18. TOASTS — branded, replaces the default black Android snackbar
   -------------------------------------------------------------------------- */
.toasts {
  position: absolute; left: var(--s-3); right: var(--s-3); z-index: 70;
  bottom: calc(var(--nav-inset) + var(--s-3));
  display: flex; flex-direction: column; gap: var(--s-2);
  pointer-events: none;
}
.toasts--high { bottom: calc(var(--safe-b) + 104px); }
.toast {
  display: flex; align-items: flex-start; gap: var(--s-3);
  padding: 13px var(--s-4);
  border-radius: var(--r-md);
  background: #181A20; color: #fff;
  box-shadow: var(--sh-3);
  pointer-events: auto;
  animation: toast-in 260ms var(--ease);
}
.toast.is-out { animation: toast-out 200ms var(--ease) forwards; }
@keyframes toast-in  { from { opacity: 0; transform: translateY(14px) scale(.97); } }
@keyframes toast-out { to   { opacity: 0; transform: translateY(10px) scale(.97); } }
.toast__icon { width: 20px; height: 20px; flex: none; margin-top: 1px; }
.toast__body { flex: 1; min-width: 0; }
.toast__title { font-size: var(--t-sm); font-weight: 800; }
.toast__text  { font-size: var(--t-sm); opacity: .76; margin-top: 1px; }
.toast__action {
  flex: none; align-self: center;
  padding: 8px 10px; margin: -8px -6px -8px 0;
  font-size: var(--t-sm); font-weight: 800; color: #7FD4DE;
  text-transform: uppercase; letter-spacing: .4px;
}
.toast--success .toast__icon { color: #5FD79A; }
.toast--error   .toast__icon { color: #F3A79C; }
.toast--warning .toast__icon { color: #E8C87A; }
.toast--info    .toast__icon { color: #7FD4DE; }

/* --------------------------------------------------------------------------
   19. OFFLINE BANNER
   -------------------------------------------------------------------------- */
.offline {
  flex: none; display: none; align-items: center; gap: var(--s-2);
  padding: 9px var(--s-4);
  background: var(--warning-bg); color: var(--warning-text);
  font-size: var(--t-sm); font-weight: 700;
  border-bottom: 1px solid #FDE68A;
}
.offline.is-on { display: flex; }
.offline svg { width: 15px; height: 15px; flex: none; }

/* --------------------------------------------------------------------------
   20. SHEET / MODAL
   -------------------------------------------------------------------------- */
.scrim {
  position: absolute; inset: 0; z-index: 50;
  background: rgba(20,22,30,.42);          /* 50% — within the 40-60% guidance */
  opacity: 0; pointer-events: none;
  transition: opacity var(--dur) var(--ease);
}
.scrim.is-open { opacity: 1; pointer-events: auto; }
.sheet {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 55;
  background: var(--surface);
  border-radius: var(--r-2xl) var(--r-2xl) 0 0;
  padding: var(--s-3) var(--s-4) calc(var(--s-5) + var(--safe-b));
  transform: translateY(100%);
  transition: transform 280ms var(--ease);
  max-height: 88%; overflow-y: auto;
}
.sheet.is-open { transform: translateY(0); }
.sheet__grip {
  width: 42px; height: 4px; border-radius: var(--r-pill);
  background: var(--border); margin: 0 auto var(--s-4);
}
.sheet__title { font-size: var(--t-h3); font-weight: 800; margin: 0 0 var(--s-1); }
.sheet__sub { font-size: var(--t-sm); color: var(--text-2); margin: 0 0 var(--s-4); }

/* Option rows inside sheets */
.opt {
  display: flex; align-items: center; gap: var(--s-3);
  width: 100%; min-height: 64px; padding: var(--s-3) var(--s-4);
  border-radius: var(--r-md); text-align: left;
  border: 1.5px solid var(--divider); background: var(--surface);
  margin-bottom: var(--s-2);
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.opt:last-child { margin-bottom: 0; }
.opt:active { background: var(--primary-50); border-color: var(--primary-d); }
.opt__icon {
  width: 40px; height: 40px; flex: none; border-radius: var(--r-sm);
  display: grid; place-items: center;
  background: var(--primary-50); color: var(--primary);
}
.opt__icon svg { width: var(--i-md); height: var(--i-md); }
.opt__body { flex: 1; min-width: 0; }
/* display:block — these are spans; without it title and sub collapse onto one line */
.opt__title { display: block; font-size: var(--t-body); font-weight: 700; line-height: var(--lh-snug); }
.opt__sub { display: block; font-size: var(--t-sm); color: var(--text-2); line-height: var(--lh-snug); margin-top: 1px; }

/* --------------------------------------------------------------------------
   20a. FROSTED SHEETS — the two sheets ported from the resident app's
   pre-approval flow share a treatment that this app's action menus do not.

   In the resident app BOTH pre-approval sheets (.pa-sheet at :746 and
   .dv-sheet at :929) are translucent and blurred over the screen behind
   them, while its plain menus are solid. That difference carries meaning:
   a frosted sheet is a place you have arrived at and will act in, a solid
   one is a short menu you will dismiss in a second. Porting the flow
   without the treatment would have kept the anatomy and lost the register.

   @supports, because backdrop-filter is the one part of this with real
   fallback consequences: without it the translucency alone would let the
   list behind show through as noise under the text.
   -------------------------------------------------------------------------- */
/* One flat tone, and nothing showing through it. The translucent version
   this replaces was faithful to the reference's blur, but on a screen with a
   photograph or a colour-blocked list behind it the sheet picked up a soft
   vertical wash — which reads as a gradient whether or not one is declared.
   A sheet is a surface you read a form on; it should be one colour.

   The colour is the reference's own sheet tint (#F7FAFB) taken at full
   opacity, so the family is unchanged: a hair cooler and lighter than the
   cards that sit on it, which is what separates the two without a border. */
.sheet--frost {
  background: #F7FAFB;
  border-top: 1px solid var(--divider);
  box-shadow: 0 -12px 46px rgba(20,16,10,.18);
}
.sheet--frost .sheet__grip { background: var(--border); }

/* --------------------------------------------------------------------------
   20c. CHOOSER SHEET — ported from the resident app's "Pre-Approve" sheet
   (reference/wave-resident-app.html, .pa-* at :746).

   This is the interaction the staff app was missing. Its New-entry FAB went
   straight to a screen whose first step was a type picker; the resident app
   asks the same question in a bottom sheet first, and only then opens the
   form on the chosen type. The difference is not cosmetic — the sheet keeps
   the guard on the list they were reading, so choosing a type is a decision
   made in place rather than a screen they have to come back from.

   Anatomy, in order:
       grab
       head   : a large left-aligned title, then one plain sentence saying
                what this is for
       card   : a rounded, hairline-separated stack of rows, each an icon, a
                label, an optional badge and a chevron
       note   : one muted row at the bottom for what is coming but not built
   -------------------------------------------------------------------------- */
.chooser__head { padding: var(--s-1) var(--s-2) 0; text-align: left; }
/* 27px, against the reference's 30. Its sheet is edge-to-edge on a wider
   canvas; at this app's width 30px wraps "New entry" onto two lines. The
   negative margin is optical, not a mistake: a large left-aligned heading
   sits visibly indented against the rows below unless its side bearing is
   pulled back. */
.chooser__head h2 {
  font-size: 27px; font-weight: 800; letter-spacing: -.9px;
  line-height: 1.05; margin: 0 0 0 -1.5px;
}
.chooser__head p {
  font-size: 12.5px; color: var(--text-2); font-weight: 500;
  margin: 7px 0 0; line-height: 1.45; max-width: 94%;
}
.chooser__card {
  margin-top: var(--s-4);
  background: var(--surface);
  border: 1px solid var(--divider);
  border-radius: 18px; overflow: hidden;
  box-shadow: var(--sh-2);
}
.chooser__opt {
  display: flex; align-items: center; gap: 14px; width: 100%;
  padding: 14px 15px; border: none; background: transparent;
  text-align: left; cursor: pointer; color: inherit;
  transition: background var(--dur) var(--ease);
}
.chooser__opt + .chooser__opt,
.chooser__soon { border-top: 1px solid var(--divider); }
.chooser__opt:active { background: rgba(12,124,140,.12); }
.chooser__ic { width: 30px; height: 30px; flex: none; display: grid; place-items: center; color: var(--text); }
.chooser__ic svg { width: 26px; height: 26px; }
.chooser__tx { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.chooser__tx b { font-size: 15px; font-weight: 700; letter-spacing: -.2px; color: var(--text); }
.chooser__chev { color: var(--text-3); flex: none; display: grid; place-items: center; }
.chooser__chev svg { width: 18px; height: 18px; }

.chooser__badge {
  font-size: 9px; font-weight: 800; letter-spacing: .05em;
  padding: 3px 7px; border-radius: 6px; text-transform: uppercase;
}
/* Was never defined — the Group row's badge rendered as an unstyled icon
   and a run of uppercase text stacked on top of each other. The reference
   uses a filled coral pill here for "New"; this one is descriptive rather
   than an announcement, so it takes the quiet info tone instead. */
.chooser__badge--new {
  display: inline-flex; align-items: center; gap: 4px;
  color: var(--info-text); background: var(--info-bg);
  text-transform: none; letter-spacing: 0; font-size: 10px;
  border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
}
.chooser__badge--new svg { width: 12px; height: 12px; }
.chooser__badge--need {
  display: inline-flex; align-items: center; gap: 4px;
  color: var(--warning-text); background: var(--warning-bg);
  text-transform: none; letter-spacing: 0;
  border: 1px solid color-mix(in srgb, currentColor 34%, transparent);
}
.chooser__badge--need svg { width: 12px; height: 12px; }

/* The "not built yet" row, from the reference's .pa-voice. Amber ground and
   no chevron, so it reads as an announcement rather than a dead option. */
.chooser__soon {
  display: flex; align-items: center; gap: 12px;
  padding: 13px 15px; background: var(--warning-bg);
}
.chooser__soon .s-ic {
  width: 38px; height: 38px; flex: none; border-radius: 50%;
  background: var(--surface); border: 1px solid var(--border);
  color: var(--warning-text); display: grid; place-items: center;
}
.chooser__soon .s-tx b {
  display: block; font-size: 11px; font-weight: 800;
  letter-spacing: .06em; text-transform: uppercase; color: var(--warning-text);
}
.chooser__soon .s-tx span {
  display: block; font-size: 12.5px; font-weight: 600; color: var(--text-2); margin-top: 1px;
}

/* --------------------------------------------------------------------------
   20d. ENTRY FORM SHEET — ported from the resident app's create-pass sheet
   (reference/wave-resident-app.html, .cf-* at :817).

   The third and last sheet in that app's pre-approval flow. Choosing a type
   in the chooser does not navigate anywhere: it swaps to THIS sheet, with
   the fields for that type, and the pass is created without the screen ever
   changing. The staff app used to hand off to visitor-new.html at this
   point, which is what put a stale Guest/Cab/Delivery picker in the back
   stack — press back after creating an entry and the old step-1 screen came
   up. No navigation, no stale screen.

   Head is an icon, a title and a sub-line, with the close button on the
   right, so the sheet says which of the five forms you are in. Body scrolls;
   foot is pinned, like the detail sheet.
   -------------------------------------------------------------------------- */
.sheet--form { display: flex; flex-direction: column; max-height: 92%; padding: var(--s-3) 0 0; overflow: hidden; }

.cfsheet__head { display: flex; align-items: center; gap: 12px; padding: 2px var(--s-5) 14px; }
.cfsheet__ic {
  width: 44px; height: 44px; flex: none; border-radius: 13px;
  background: var(--surface); border: 1px solid var(--border);
  color: var(--primary); display: grid; place-items: center;
  box-shadow: 0 2px 8px rgba(20,22,30,.08);
}
.cfsheet__ic svg { width: 26px; height: 26px; }
.cfsheet__tx { flex: 1; min-width: 0; }
.cfsheet__tx h3 { font-size: 17px; font-weight: 800; letter-spacing: -.3px; margin: 0; }
.cfsheet__tx p { font-size: 11.5px; color: var(--text-2); font-weight: 500; margin: 2px 0 0; }

.cfsheet__body {
  overflow-y: auto; scrollbar-width: none;
  padding: var(--s-2) var(--s-5) var(--s-4);
  /* A wrapping flex row, not a stack: it is what lets a field opt into a
     half column with one class, so date/time and count/vehicle pairs sit on
     one line instead of each burning a full row of a sheet that is already
     fighting for height. */
  display: flex; flex-wrap: wrap; gap: 14px 10px;
}
.cfsheet__body::-webkit-scrollbar { display: none; }
.cfsheet__body .field { flex: 1 1 100%; min-width: 0; margin-bottom: 0; }
/* flex-grow 0. With grow 1 a half field that has no sibling on its row —
   "No. of people", "Parcels" — expands to the full width anyway, which
   makes the half/full rhythm look accidental rather than chosen. The
   reference has the same behaviour; this is a deliberate departure. */
.cfsheet__body .field--half { flex: 0 1 calc(50% - 5px); }
.cfsheet__body .field.is-hidden { display: none; }

/* The flat, repeated in the form head. Not decoration: three fields in, a
   guard has stopped looking at the picker, and every one of these forms is
   addressed to one specific resident. It is also the only way back to change
   it. */
/* Who is being asked. The first build of this ran the flat and the name
   together on one line with a house icon beside them, which read as an
   address rather than as a person — and an address is not what a guard is
   about to interrupt. Rebuilt around the resident's face, with the label
   ("Asking"), the name and the flat on three lines of clearly different
   weight, so the person leads and the address supports.

   The three lines are each display:block. Left inline they collapse into
   "A-406 · Tower ANam Dev Sharma", which is exactly what happened. */
.cfsheet__for {
  display: flex; align-items: center; gap: 11px; flex: none;
  margin: 0 var(--s-5) var(--s-3); padding: 9px 11px 9px 9px;
  background: var(--primary-wash); border-radius: var(--r-md);
}
.cfsheet__for-av {
  width: 40px; height: 40px; flex: none; border-radius: 50%;
  overflow: hidden; background: var(--surface);
  display: grid; place-items: center;
  font-size: 15px; font-weight: 800; color: var(--primary-d);
}
.cfsheet__for-av img { width: 100%; height: 100%; object-fit: cover; }
.cfsheet__for-tx { flex: 1; min-width: 0; }
.cfsheet__for-k {
  display: block; font-size: 9.5px; font-weight: 800; letter-spacing: .7px;
  text-transform: uppercase; color: var(--primary-d); opacity: .75;
}
.cfsheet__for-tx b {
  display: block; font-size: 14px; font-weight: 800; color: var(--text);
  line-height: 1.2; margin-top: 1px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cfsheet__for-s {
  display: block; font-size: 11.5px; color: var(--primary-d);
  font-weight: 600; margin-top: 1px;
}
.cfsheet__for-ch {
  flex: none; font-size: 12px; font-weight: 800; color: var(--primary-d);
  padding: 6px 10px; border-radius: var(--r-pill); background: rgba(255,255,255,.7);
}
.cfsheet__for-ch:active { background: #fff; }

/* Flat picker rows. Lifted out of visitor-new.html, which had them in a
   screen-local <style> — the picker is a shared step now, used by all five
   entry flows, so it cannot live inside one screen. */
.flatpick__list { display: block; }
.flat {
  display: flex; align-items: center; gap: var(--s-3);
  width: 100%; padding: var(--s-3); min-height: 68px;
  background: var(--surface); border: 1.5px solid var(--divider);
  border-radius: var(--r-md); text-align: left; margin-bottom: var(--s-2);
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.flat:active { border-color: var(--primary); background: var(--primary-50); }
.flat__av {
  width: 44px; height: 44px; flex: none; border-radius: 50%;
  overflow: hidden; background: var(--primary-wash);
  display: grid; place-items: center;
  font-size: 15px; font-weight: 800; color: var(--primary-d);
}
.flat__av img { width: 100%; height: 100%; object-fit: cover; }
.flat__no { display: block; font-size: var(--t-body); font-weight: 800; line-height: var(--lh-snug); }
.flat__r { display: block; font-size: var(--t-sm); color: var(--text-2); margin-top: 2px; }
.flat__go { flex: none; color: var(--text-3); display: grid; place-items: center; }
.flat__check {
  width: 24px; height: 24px; border-radius: 50%; border: 2px solid var(--border);
  flex: none; display: grid; place-items: center; color: transparent;
}
.flat[aria-pressed="true"] { border-color: var(--primary); background: var(--primary-50); }
.flat[aria-pressed="true"] .flat__check { background: var(--primary); border-color: var(--primary); color: #fff; }

.cfsheet__foot {
  flex: none; padding: var(--s-3) var(--s-5) calc(var(--s-4) + var(--safe-b));
  border-top: 1px solid var(--divider);
  background: #F7FAFB;
}

/* ---- Two controls this app did not have --------------------------------
   Everything else the five forms need already exists — .field, .stepper and
   .segment are all in section 13. These two are new. */

/* Single-select chips, for a list too long for a segmented control and too
   short for a dropdown: delivery partners, cab services, visit purposes. */
.fchips { display: flex; flex-wrap: wrap; gap: 8px; }
.fchip {
  padding: 9px 14px; border-radius: var(--r-pill);
  border: 1.5px solid var(--divider); background: var(--surface);
  font-size: 12.5px; font-weight: 600; color: var(--text-2);
  cursor: pointer; transition: background var(--dur) var(--ease),
                               border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.fchip:active { transform: scale(.96); }
.fchip[aria-pressed="true"] {
  background: var(--primary-wash); border-color: var(--primary); color: var(--primary-d);
}

/* Brand pill — the resident app's .brand (reference:859). A logo beside the
   name, not a name alone: at a gate the guard is matching a jacket or a bag
   against this list, and the mark is what they are matching. */
.brand {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 6px 13px 6px 6px; border-radius: var(--r-pill);
  border: 1.5px solid var(--divider); background: var(--surface);
  font-size: 12.5px; font-weight: 700; color: var(--text-2);
  cursor: pointer; transition: background var(--dur) var(--ease),
                               border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.brand:active { transform: scale(.96); }
.brand__logo {
  width: 24px; height: 24px; flex: none; border-radius: 7px;
  object-fit: contain; background: var(--surface);
}
/* Lettermark, for the two options with no logo file: "Other", which never
   had one, and Ola, whose favicon that service will not return. Better a
   deliberate initial than a broken image. */
.brand__letter {
  width: 24px; height: 24px; flex: none; border-radius: 7px;
  background: var(--surface-2); color: var(--text-3);
  display: grid; place-items: center; font-size: 13px; font-weight: 700;
}
.brand[aria-pressed="true"] {
  background: var(--primary-wash); border-color: var(--primary); color: var(--primary-d);
}
.brand[aria-pressed="true"] .brand__letter { background: var(--surface); color: var(--primary-d); }

/* A labelled switch on its own ground, so it reads as a setting rather than
   as a button that happens to be round. */
.ftoggle {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  background: var(--surface-2); border: 1px solid var(--divider);
  border-radius: var(--r-md); padding: 11px 13px; width: 100%;
}
.ftoggle__lbl { font-size: 13px; font-weight: 600; color: var(--text-2); text-align: left; }
.ftoggle__sw {
  width: 44px; height: 26px; flex: none; border-radius: var(--r-pill);
  background: var(--border); position: relative;
  transition: background var(--dur) var(--ease);
}
.ftoggle__sw::after {
  content: ''; position: absolute; top: 3px; left: 3px;
  width: 20px; height: 20px; border-radius: 50%; background: #fff;
  box-shadow: var(--sh-1); transition: transform var(--dur) var(--ease);
}
.ftoggle[aria-pressed="true"] .ftoggle__sw { background: var(--primary); }
.ftoggle[aria-pressed="true"] .ftoggle__sw::after { transform: translateX(18px); }

/* --------------------------------------------------------------------------
   20b. DETAIL SHEET — ported from the resident app's pre-approval sheet
   (reference/wave-resident-app.html, .dv-* at :929).

   That app opens a pre-approval as a BOTTOM SHEET rather than a screen, and
   the anatomy is specific:

       grab handle
       round close button, floating top-right
       ---- scrolls ----------------------------------
       hero      : large circular avatar, name, type badge, all centred
       groups    : key/value rows, each row an icon + muted label on the
                   left and a bold value on the right; groups separated by
                   a hairline, never by a heading
       note      : one quiet underlined link for the "what if this is
                   wrong" case
       ---- pinned -----------------------------------
       foot      : exactly two buttons, one outline and one filled

   The sheet mechanics themselves needed nothing: this app's .scrim and
   .sheet already use the same scrim tint, the same translateY(100%) entry
   and the same ~280ms ease as the resident app's. What is added here is the
   INTERNAL structure, plus the one thing .sheet cannot do — .sheet scrolls
   itself, which would carry the footer buttons off the bottom. A detail
   sheet is a flex column with a scrolling middle and a pinned foot.
   -------------------------------------------------------------------------- */
.sheet--detail {
  display: flex; flex-direction: column;
  max-height: 92%;
  padding: var(--s-3) 0 0;   /* sides belong to the scroller, so the hairline
                                between groups can run edge to edge */
  overflow: hidden;          /* the middle scrolls, not the sheet */
}
.dsheet__close {
  position: absolute; top: 14px; right: 14px; z-index: 3;
  width: 34px; height: 34px; border-radius: 50%;
  display: grid; place-items: center;
  border: 1px solid var(--border); background: var(--surface);
  color: var(--text-2); cursor: pointer;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.dsheet__close:active { transform: scale(.92); }
/* Red on hover, and red while it is being pressed. Closing one of these
   sheets throws away whatever has been typed into it, and the reference
   colours this control accordingly (.cf-close / .dv-x are coral on hover).
   It is the only destructive control on the sheet, so it is the only one
   that changes colour. */
.dsheet__close:hover,
.dsheet__close:focus-visible { border-color: var(--error); background: var(--error-bg); color: var(--error-text); }
.dsheet__close:active { border-color: var(--error); background: var(--error-bg); color: var(--error-text); transform: scale(.92); }
.dsheet__close svg { width: 17px; height: 17px; }

.dsheet__scroll { overflow-y: auto; padding: 0 var(--s-5); scrollbar-width: none; }
.dsheet__scroll::-webkit-scrollbar { display: none; }

.dsheet__hero { text-align: center; padding: var(--s-1) 0 var(--s-2); }
/* 88px disc, same treatment as the visitor card's 46px one — white initials
   on the type colour — so the sheet is visibly the same record enlarged. */
.dsheet__av {
  width: 88px; height: 88px; border-radius: 50%; margin: 0 auto;
  display: grid; place-items: center;
  color: #fff; font-weight: 700; font-size: 29px; letter-spacing: .5px;
}
.dsheet__nm { font-size: 22px; font-weight: 800; letter-spacing: -.5px; margin-top: 11px; }
.dsheet__hero .chip { margin-top: 9px; }

.dsheet__grp { padding: var(--s-1) 0; }
.dsheet__grp + .dsheet__grp { border-top: 1px solid var(--divider); }
/* A fixed label column, not auto. Ragged label widths turn a list of facts
   into a list of sentences; one column edge lets the eye run down the
   values, which is what a guard is actually doing. */
.dsheet__row {
  display: grid; grid-template-columns: 132px 1fr;
  align-items: center; gap: 10px; padding: 11px 0;
}
.dsheet__key { display: flex; align-items: center; gap: 9px; min-width: 0; }
.dsheet__ic { flex: none; color: var(--text-3); display: grid; place-items: center; }
.dsheet__ic svg { width: 17px; height: 17px; }
.dsheet__lbl { font-size: 13px; color: var(--text-2); font-weight: 500; white-space: nowrap; }
.dsheet__val {
  min-width: 0; display: flex; align-items: center; justify-content: space-between;
  gap: 9px; font-size: 13px; color: var(--text); font-weight: 700;
}
.dsheet__val > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dsheet__copy { flex: none; color: var(--primary); cursor: pointer; display: grid; place-items: center; }
.dsheet__copy:active { transform: scale(.9); }
.dsheet__copy svg { width: 15px; height: 15px; }

.dsheet__note {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  color: var(--warning-text); font-weight: 700; font-size: 12.5px;
  padding: var(--s-4) 0 var(--s-3); cursor: pointer;
  text-decoration: underline; text-underline-offset: 2px;
}
.dsheet__note svg { width: 16px; height: 16px; flex: none; }

/* Pinned. Two buttons, one outline and one filled — the resident app's rule,
   and the same "one primary per region" this app already follows. */
.dsheet__foot {
  display: flex; gap: var(--s-3); flex: none;
  padding: var(--s-3) var(--s-4) calc(var(--s-4) + var(--safe-b));
  border-top: 1px solid var(--divider);
  background: #F7FAFB;
}
/* Equal halves, as in the resident app (.dv-btn is flex:1 there). Sized by
   content instead, the outline button collapsed until "Call resident"
   wrapped onto two lines inside it while the filled button took the rest of
   the row. Two actions of equal standing get equal width. */
.dsheet__foot .btn { flex: 1 1 0; min-width: 0; }

/* --------------------------------------------------------------------------
   21. STAT TILES — shared tile styling; the home KPI row that introduced it
   is gone, but profile.html's duty-stats strip (.pstats) still consumes
   .stat/.stat__num/.stat__label/.stat--live, so those stay.
   -------------------------------------------------------------------------- */
.stat {
  background: var(--surface); border-radius: var(--r-md);
  padding: var(--s-4) var(--s-2) var(--s-3);
  text-align: center; box-shadow: var(--sh-2);
  position: relative; display: block;
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.stat:active { transform: scale(.97); box-shadow: var(--sh-1); }
.stat__num { font-size: var(--t-h1); font-weight: 800; line-height: 1.1; letter-spacing: -.8px; }
.stat__label { font-size: var(--t-xs); color: var(--text-2); font-weight: 700; margin-top: 3px; letter-spacing: .2px; }
/* One accent per row: the number carries the state, the label stays neutral */
.stat--live  .stat__num { color: var(--primary-d); }

/* --------------------------------------------------------------------------
   22. SEARCH BAR — lifted from the resident app home screen
   -------------------------------------------------------------------------- */
/* Sits on the light page again — in home's header, in the row that replaces
   the identity block when the search icon is pressed. The white-halo states
   it wore while the hero was teal are gone with the teal: a white ring on a
   near-white page is invisible, so hover and focus are back on the border
   and the standard --ring. */
/* White, not --surface-2. The field sat at #F1F5F6 on a #EAF0F1 page — a
   four-point difference on every channel, which is the same not-quite-there
   container the empty-state disc and the old .tabs strip both had. A search
   bar is the one thing on Home a guard goes looking for with a person
   standing in front of them; it has to be findable without being hunted.
   White ground, the stronger --border hairline, and the shadow the card
   below it uses — so it reads as a field ON the page rather than a slightly
   different shade OF it. */
.searchbar {
  display: flex; align-items: center; gap: 11px;
  margin: 0 var(--s-4); padding: 12px 12px 12px 16px;
  background: var(--surface); border: 1px solid var(--border);
  box-shadow: var(--sh-1);
  border-radius: 16px; color: var(--text-3); cursor: text;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease);
}
/* Hover — a real hint that the field is interactive, before the tap: the
   card lifts a touch and brightens to full white. This is a touch-first
   prototype but shown on desktop for the demo audience, so hover earns its
   own state rather than reusing focus's. */
/* The base is already white, so hover cannot signal by filling in. It lifts
   instead — a deeper shadow and a pixel of travel. */
.searchbar:hover {
  box-shadow: var(--sh-2);
  transform: translateY(-1px);
}
/* Focus — must hold on its own; touch has no hover to fall back on. On the
   light page --ring (3px of --primary-wash) separates cleanly, and the
   teal border says which field has the caret. */
.searchbar:focus-within {
  background: var(--surface); border-color: var(--primary);
  box-shadow: var(--ring), var(--sh-2);
  transform: translateY(-1px);
  color: var(--text-2);
}
.searchbar > svg { flex: none; }
.searchbar input {
  flex: 1; min-width: 0; border: none; outline: none; background: none;
  font-family: inherit; font-size: 14px; font-weight: 500; color: var(--text);
}
.searchbar input::placeholder { color: var(--text-3); font-weight: 500; }
.searchbar .mic {
  width: 34px; height: 34px; border-radius: 50%;
  background: var(--primary-wash); color: var(--primary-d);
  display: grid; place-items: center; flex: none; cursor: pointer;
  border: none; transition: transform .16s var(--ease);
}
.searchbar .mic:active { transform: scale(.9); }

/* Icon beds — Wave uses ONE bed colour everywhere (primary-wash + primary-d),
   not a per-module rainbow. Recognition comes from the glyph. m-visitors,
   m-verify and m-delivery back visitor-type avatars (see TYPE() in app.js
   and visitorCard()); the module-grid-only beds were removed with the grid. */
.m-visitors,
.m-verify,
.m-delivery { background: var(--primary-wash); color: var(--primary-d); }

/* --------------------------------------------------------------------------
   22b. OUTCOME BANNER
   Ported from the resident app's .rdd-status (reference/wave-resident-app.html
   :2058), which is how that app announces a settled outcome — ride completed,
   ride cancelled.

   Why this shape and not a big centred badge: the staff approval screen used
   to open each outcome with a 104px pale circle, a centred h1 and a centred
   caption. That is a full-screen celebration for something that is one line
   of fact ("Nam Dev Sharma approved at 11:22"), it pushed the gate pass —
   the only thing a guard actually needs to read out — below the fold, and it
   made denial and approval visually identical apart from hue.

   The banner says the same thing in a strip: filled, so it reads as an
   answered state rather than another empty container; left-aligned, so the
   eye lands on the words in reading order instead of bouncing to a centred
   axis and back; and small enough that whatever the guard must act on next
   sits directly beneath it.
   -------------------------------------------------------------------------- */
.outcome {
  display: flex; align-items: center; gap: 13px;
  padding: 15px 16px; border-radius: var(--r-lg);
  color: #fff; box-shadow: var(--sh-2);
  animation: outcome-in 320ms var(--ease) both;
}
@keyframes outcome-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
/* Flat fills, not gradients — the resident app gradates this strip, but this
   app's own direction (design-system.css, section 1) is no gradients outside
   the header, and one exception here would read as an accident. Each tone is
   the darkened, AA-safe text colour from the semantic pairs above, used as a
   FILL with white on it, so the family stays the app's own. */
.outcome--ok   { background: var(--success-text); }
.outcome--no   { background: #B23A2C; }   /* --error-text is a text tone; this is its fill twin */
.outcome--warn { background: var(--warning-text); }
.outcome--info { background: var(--primary-d); }
.outcome--idle { background: var(--text-2); }
/* 12px radius on a 42px square, matching the resident app: a squircle beside
   round type reads as deliberate where a second circle reads as a repeat. */
.outcome__ico {
  width: 42px; height: 42px; flex: none; border-radius: 12px;
  background: rgba(255,255,255,.18);
  display: grid; place-items: center;
}
.outcome__b { flex: 1; min-width: 0; }
.outcome__t { display: block; font-size: 14.5px; font-weight: 800; letter-spacing: -.2px; }
.outcome__s { display: block; font-size: 11.5px; font-weight: 500; opacity: .92; margin-top: 2px; line-height: 1.4; }

/* --------------------------------------------------------------------------
   22b2. CONFIRMATION CARD — ported from the resident app's booking-confirmed
   screen (reference/wave-resident-app.html, .done-* at :1236).

   That screen is the one place that app stops and tells you a thing is
   settled: a large filled disc with a white glyph, a heading, one line of
   plain explanation, and a bordered pill carrying the reference you would
   read out. What makes it land is the disc's DOUBLE shadow —

       box-shadow: 0 16px 42px rgba(tone,.42), 0 0 0 12px rgba(tone,.1);

   a deep coloured drop plus a soft 12px ring. The ring is what reads as a
   halo around the mark; it is static, not animated, and does the work that a
   pulse would without anything moving.

   The approval screen replaces its filled banner strip with this. A banner is
   right for something you skim past; every one of these six states is a full
   stop — the resident has answered, or has not, and the guard's next move
   changes completely. The reference is worth following on that.

   Tones are named, not coloured, at the call site. Each sets --tone (the
   fill) and --tone-rgb (the shadows, which need the channels).
   -------------------------------------------------------------------------- */
/* A white rounded card, not a bare block on the page ground. The reference's
   booking-confirmed screen fills a whole screen with its tick, which works
   because that screen has nothing else on it. This one carries the visitor
   summary and the approval timeline underneath, so the outcome needs an edge
   of its own — without one the badge and its two lines float in the gap above
   the first card and read as a header rather than as the answer. */
.confirm {
  display: flex; flex-direction: column; align-items: center;
  text-align: center; padding: var(--s-6) var(--s-4) var(--s-5);
  background: var(--surface);
  border: 1px solid var(--divider);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-2);
}
.confirm__badge {
  width: 92px; height: 92px; border-radius: 50%;
  display: grid; place-items: center; color: #fff;
  background: var(--tone);
  box-shadow: 0 16px 42px rgba(var(--tone-rgb), .42),
              0 0 0 12px rgba(var(--tone-rgb), .10);
  animation: confirm-pop .45s var(--ease) both;
}
.confirm__badge svg { width: 44px; height: 44px; }
@keyframes confirm-pop {
  0%   { transform: scale(.3); opacity: 0; }
  60%  { transform: scale(1.09); }
  100% { transform: scale(1); opacity: 1; }
}
/* The ring is a shadow, so it costs no layout. But the disc still needs room
   for it or the card above crowds the halo — 12px of ring plus a little. */
.confirm__badge { margin-bottom: calc(var(--s-6) + 4px); }
@media (prefers-reduced-motion: reduce) { .confirm__badge { animation: none; } }

.confirm__t {
  font-size: 22px; font-weight: 800; letter-spacing: -.5px;
  line-height: 1.2; color: var(--text); margin: 0;
}
.confirm__s {
  font-size: 13px; color: var(--text-2); font-weight: 500;
  line-height: 1.55; margin: 9px 0 0; max-width: 30ch;
}
/* The bordered pill under the message — the reference's .done-id. Carries the
   one value a guard reads out or watches: a countdown while waiting, a pass
   number once approved. */
.confirm__id {
  display: inline-flex; align-items: center; gap: 8px;
  margin-top: var(--s-5); padding: 11px 18px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 12px;
  font-size: 12.5px; color: var(--text-2); font-weight: 600;
}
.confirm__id b {
  color: var(--text); font-weight: 800; font-variant-numeric: tabular-nums;
  letter-spacing: .2px;
}
.confirm__id svg { width: 15px; height: 15px; flex: none; color: var(--text-3); }

.confirm--ok   { --tone: var(--success-text); --tone-rgb: 20, 102, 63; }
.confirm--no   { --tone: #B23A2C;             --tone-rgb: 178, 58, 44; }
.confirm--wait { --tone: var(--warning-text); --tone-rgb: 140, 93, 17; }
.confirm--info { --tone: var(--primary-d);    --tone-rgb: 10, 95, 108; }
.confirm--idle { --tone: var(--text-2);       --tone-rgb: 71, 74, 83; }

/* The waiting state's countdown is the one number on that screen a guard
   actually watches, so it is set larger than the pill's other values and
   goes red in the last thirty seconds — the same threshold the card timers
   use, so "nearly out of time" looks the same everywhere in the app. */
.confirm__id--timer b { font-size: 16px; }
.confirm__id--timer.is-urgent { border-color: var(--error); background: var(--error-bg); }
.confirm__id--timer.is-urgent b,
.confirm__id--timer.is-urgent svg { color: var(--error-text); }

/* --------------------------------------------------------------------------
   22c. QUOTE — a resident's own words, on a denial or a reassignment.
   The old markup nested a .card inside a centred .stage, overrode its
   background to --error-bg inline, and left the text centred by inheritance
   while the label above it was not. A quote is prose: it needs a consistent
   left edge and a mark that shows where it starts. This gives it both, and
   takes the alignment off inheritance so no parent can re-centre it.
   -------------------------------------------------------------------------- */
.quote {
  position: relative; text-align: left;
  background: var(--error-bg); border: 1px solid rgba(224,89,74,.22);
  border-radius: var(--r-md); padding: var(--s-4) var(--s-4) var(--s-4) 44px;
}
/* The quotation mark is the bullet, so the label and the quoted line share
   one left edge instead of the label hanging off on its own. */
.quote::before {
  content: '\201C';
  position: absolute; left: 14px; top: 10px;
  font-size: 34px; line-height: 1; font-weight: 800;
  color: var(--error-text); opacity: .34;
}
.quote__k {
  display: block; font-size: var(--t-xs); font-weight: 800;
  letter-spacing: .6px; text-transform: uppercase;
  color: var(--error-text); opacity: .85; margin-bottom: 4px;
}
.quote__v {
  display: block; font-size: var(--t-body); font-weight: 600;
  line-height: 1.45; color: var(--error-text);
}
.quote--info { background: var(--primary-wash); border-color: rgba(12,124,140,.2); }
.quote--info::before, .quote--info .quote__k, .quote--info .quote__v { color: var(--primary-d); }

/* --------------------------------------------------------------------------
   22d. INLINE NOTE — an instruction that follows an outcome, e.g. what a
   guard must do when a resident never answered. Distinct from .quote: a
   quote carries someone else's words, a note carries the app's own
   instruction, so it gets an icon rather than a quotation mark.
   -------------------------------------------------------------------------- */
.note {
  display: flex; align-items: flex-start; gap: 10px;
  margin-top: var(--s-3); padding: 13px 14px;
  border-radius: var(--r-md); text-align: left;
  background: var(--surface-2); border: 1px solid var(--divider);
}
/* The icon box is one line of the paragraph tall (13px x 1.45 = 18.85px) and
   centres its glyph in that box, so it sits on the first line rather than on
   the block's top edge — the same rule .qa-soon__icon uses. */
.note > span, .note svg { flex: none; }
.note > span {
  width: 18px; height: 18.85px;
  display: grid; place-items: center;
  color: var(--text-3);
}
.note p { margin: 0; font-size: 13px; font-weight: 600; line-height: 1.45; color: var(--text-2); }
.note--warn { background: var(--warning-bg); border-color: rgba(184,134,43,.28); }
.note--warn > span { color: var(--warning-text); }
.note--warn p { color: var(--warning-text); }

/* --------------------------------------------------------------------------
   23. COUNTDOWN TIMER (shared by home + visitor list)
   -------------------------------------------------------------------------- */
.timer {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 4px 9px; border-radius: 999px;
  font-size: var(--t-sm); font-weight: 800;
  font-variant-numeric: tabular-nums;
  background: var(--warning-bg); color: var(--warning-text);
  /* flex:none — sits beside the eyebrow in a `.row.between`; it must never
     be the side that gives up space. white-space:nowrap keeps its own
     label (e.g. "No response" once a countdown times out) from breaking
     mid-pill; the eyebrow next to it (.vcard__type, above) is set up to
     shrink and truncate first, so this pill wins the room it needs. */
  flex: none; white-space: nowrap;
}
.timer svg { width: 13px; height: 13px; }
.timer.is-urgent { background: var(--error-bg); color: var(--error-text); }

/* The arrival window on a resident pre-approval. It occupies the same slot
   as .timer but is deliberately NOT a variant of it: a timer is a deadline
   that gets more urgent as it runs down, a window is just information about
   when someone is due. So it borrows none of .timer's escalation — no amber
   bed, no is-urgent, no tabular countdown — and reads as the quiet neutral
   fact it is. Sharing .timer's class would have made it one restyle away
   from inheriting urgency it can never have. */
.window {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 9px; border-radius: 999px;
  font-size: var(--t-xs); font-weight: 700;
  background: var(--surface-2); color: var(--text-2);
  border: 1px solid var(--divider);
  flex: none; white-space: nowrap;
}
.window svg { width: 12px; height: 12px; flex: none; color: var(--text-3); }

/* --------------------------------------------------------------------------
   24. KEY/VALUE REVIEW ROWS
   -------------------------------------------------------------------------- */
.kv { display: flex; justify-content: space-between; gap: var(--s-4); padding: 11px 0; }
.kv + .kv { border-top: 1px solid var(--divider); }
.kv__k { font-size: var(--t-sm); color: var(--text-2); flex: none; }
.kv__v { font-size: var(--t-sm); font-weight: 700; text-align: right; }

/* --------------------------------------------------------------------------
   25. UTILITIES
   -------------------------------------------------------------------------- */
.row { display: flex; align-items: center; gap: var(--s-3); }
.row--top { align-items: flex-start; }
.between { justify-content: space-between; }
.grow { flex: 1; min-width: 0; }
.wrap { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hidden { display: none !important; }
.divider { height: 1px; background: var(--divider); margin: var(--s-3) 0; }
.stack > * + * { margin-top: var(--s-3); }
.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;
}

/* ==========================================================================
   26. HOME — GATE STATS + QUICK ACTIONS
   Replaces the tabbed "Smart Hub" this screen used to carry.

   Why the hub went: it was a tab strip whose four tabs each revealed a
   different panel in place. That is a browsing control, and Home is not a
   place a guard browses — they arrive at it between people at a barrier,
   with the phone at arm's length, often in direct sun. A tab strip made
   every destination except one invisible until tapped, put four 11px
   two-line labels in a row, and hid the register's own status counts one
   level down inside a panel.

   What replaces it is two clearly different things, deliberately not
   styled alike:

     .gstats    — three NUMBERS. Read-only status of today's register,
                  glanceable, no decision attached.
     .qgrid     — four TASKS. Big targets you press to go somewhere.

   Numbers are compact and centred; tasks are large, left-aligned and
   card-like. A guard should be able to tell which is which without reading
   either — that is the "distinguished" part.
   ========================================================================== */

/* ---- Three white cards, laid out on SS/Screenshot 2026-08-31 165736.png --
   The reference is three separate cards in a row, each a centred stack of a
   dual-colour mark, a value and a label. This block follows it: same 3-up
   grid at the same 10px gap, but every cell is now a card in its own right
   rather than a tinted panel borrowed from the resident app's Wallet
   Consumption Details component.

   What changed, and why:

     • The bed is white. The three given fills — #FBF7D5 / #DBF5D8 / #FFDBDB
       — sat 1.01-1.11 to 1 against the page, which is exactly why they had
       to be wrapped in a white .card to be seen at all. A white cell with
       the app's own 1px --divider edge and --sh-1 needs no such wrapper, so
       the wrapper is gone and the cards sit directly on the page.
     • Content is centred, not ranged left. That is the reference's
       composition and the brief's: mark, then value, then label, each one
       centred on the same axis.
     • The mark sits on the resident app's day-card icon bed. Reference:
       .dayc .di (reference/wave-resident-app.html:233) — a 34px square at a
       10px radius, filled --primary-wash, holding a 20px duotone glyph. It
       replaces the 34px SOLID chip that used to sit here carrying an 18px
       white outline: same footprint, but a light bed under a drawing that
       keeps its own two colours instead of a saturated bed that flattened
       the glyph to one.
     • The glyph itself is SS/awaiting.svg, SS/Approved.svg and
       SS/denied.svg ported verbatim (VIS_ICONS in app.js). The bed sets
       both halves of their duotone: currentColor takes --text, and
       --ic-accent takes --primary-2, the teal those files were drawn in.
     • The unit line is gone. Icon, count, heading — three elements, which
       is what a 107px column sets cleanly at this scale.

   Accessibility is unchanged in substance: three differently-SHAPED glyphs,
   a written label on each, and both text tones at --text on white (13.15:1).
   Colour is not a carrier here at all now, which is the strongest version of
   the rule the old cells were already following. */
.gstats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.gstat {
  background: var(--surface);
  border: 1px solid var(--divider);
  border-radius: 16px;            /* the card radius, because it is a card */
  box-shadow: var(--sh-1);
  padding: 16px 6px 14px;
  display: flex; flex-direction: column; align-items: center;
  text-align: center; color: inherit;
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.gstat:active { transform: scale(.96); }
.gstat:hover  { box-shadow: var(--sh-2); }

/* The icon bed, lifted from .dayc .di in the resident app: 34px square, 10px
   radius, --primary-wash, glyph centred at 20px. Every number is the
   reference's (reference/wave-resident-app.html:233), including the absence
   of a shadow — that app's --sh-ic resolves to none.

   The bed sets BOTH halves of the ported drawings' duotone. currentColor is
   the ink, so the black the SS files were saved with becomes --text; and
   --ic-accent is pinned to --primary-2, the app's highlight teal and the
   nearest token to the #33CCCC those files were drawn in. The reference's
   own .di paints its glyph --primary-d, which would have put deep teal line
   work and a teal accent on a teal wash — three teals and no drawing. */
.gstat__ic {
  --ic-accent: var(--primary-2);
  color: var(--text);
  width: 34px; height: 34px; flex: none;
  border-radius: 10px;
  background: var(--primary-wash);
  display: grid; place-items: center;
}
.gstat__ic svg { width: 20px; height: 20px; display: block; }

/* Value, then label — the reference's order, and the one the brief asked
   for. Both take Wave's heading ink; the hierarchy is scale and weight, not
   a second colour, so the pair holds together as one block of type. */
.gstat__n {
  font-size: 24px; font-weight: 800; line-height: 1;
  color: var(--text); font-variant-numeric: tabular-nums;
  margin-top: 11px;   /* .dayc .dttl's gap under the bed */
}
.gstat__k {
  font-size: 13px; font-weight: 700; line-height: 1.2;
  color: var(--text); margin-top: 6px;
}

/* ---- Quick actions: a three-up bento -----------------------------------
   Structure and proportions read off SS/Quick Actions Layout.png. The
   reference is 647px wide; every ratio below is taken from it relative to a
   card's own width, then applied at this app's 172px card width, so the
   composition survives the change of scale:

       card gap        : card width  =  23 : 282  ->  13px
       stacked card h  : card width  = 217 : 290  -> 120px
       tall card h     : card width  = 456 : 282  -> 253px (= 120 x 2 + 13)
       stacked foot h  : card height =  80 : 217  ->  44px
       tall foot h     : card height = 106 : 456  ->  59px

   Those are derived against a card of 161px, which is what the rendered
   page actually measures — not the 172px the arithmetic suggests. The
   difference is small but it compounds: sized off 172 the grid came out
   17px too tall, which is most of the reason the New-entry FAB was clipping
   the bottom of the third card.

   That last pair is the detail an eyeball misses: the tall card's foot is
   NOT the same height as the stacked cards' feet. It is half again as deep,
   because its title is bigger and it carries more padding. Building all
   three with one foot rule made the tall card read as a small card that had
   been stretched, rather than as the primary one.

       +-------------+------------+
       |             | Daily      |
       | Inside now  +------------+
       |             | Kids       |
       +-------------+------------+

   Everything else the reference does: photo full-bleed to the card's top
   three edges, white foot beneath with text left and a round-square go
   button right, count pip floating on the photo. The one FILLED go button
   on the screen belongs to the tall card — same "one primary per region"
   rule the CTA bars follow.

   Three modules, not four. Service visits came out: it is a society
   maintenance queue rather than something a guard does at a barrier. All
   three carry identical treatment; the two that are not built yet are not
   fenced off with a dashed edge or a "Soon" tag — they say what they are,
   and say where they stand when pressed. */
.qgrid {
  display: grid; gap: 13px;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 120px 120px;
}
.qtile--tall { grid-row: span 2; }

.qtile {
  position: relative; display: flex; flex-direction: column;
  text-align: left; overflow: hidden;
  /* padding:0 — the photo is full-bleed to three edges, so the card's own
     radius is what rounds it. The foot carries the inset instead. */
  padding: 0;
  background: var(--surface);
  border: 1px solid var(--divider); border-radius: 16px;
  box-shadow: var(--sh-2); color: inherit;
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.qtile:active { transform: scale(.97); }
.qtile:hover { box-shadow: var(--sh-3); }

/* The photo takes whatever height the cell has left after the foot, so the
   tall cell's image grows into it. object-fit does the cropping, which is
   why the source files can be one plain download each: the crop follows the
   layout instead of being baked into the file. --pos is the per-image
   focal point — see the tiles in index.html. */
.qtile__cover {
  position: relative; flex: 1 1 auto; min-height: 0;
  background: var(--primary-wash);
  overflow: hidden;
}
.qtile__cover img {
  display: block; width: 100%; height: 100%;
  object-fit: cover; object-position: var(--pos, center);
}
/* The photo does not stop at a hard line above the text — it dissolves into
   it. Without this the card reads as two stacked panels, a picture and a
   label, with a visible seam between them; with it the surface of the foot
   appears to continue up into the image and the two read as one object.

   Fading to --surface (not to white) so it stays correct if the surface
   token ever moves. The tall cell fades over more distance because it has
   more image to give: 36px of 193 is a soft landing, whereas 36 of a 73px
   band would swallow half the photograph. */
/* A longer, softer fade than the first attempt. Two linear stops go from
   nothing to solid over their whole length, which puts a visible horizon
   partway up the photo — the eye finds the point where white starts. Easing
   the stops (slow at the top, fast at the bottom) means most of the fade
   happens in the last few pixels and the top of it is imperceptible, so the
   photo dissolves rather than meeting a soft edge. The distance is longer
   for the same reason. */
.qtile__cover::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 38px;
  background: linear-gradient(to bottom,
    rgba(255,255,255,0) 0%,
    rgba(255,255,255,.18) 42%,
    rgba(255,255,255,.55) 68%,
    rgba(255,255,255,.86) 86%,
    var(--surface) 100%);
  pointer-events: none;
}
.qtile--tall .qtile__cover::after { height: 62px; }

.qtile__foot {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 9px 7px 11px;
  background: var(--surface);
}
/* Deeper, per the reference — see the ratio table above. */
/* The tall foot is deeper than the stacked ones, per the reference. The
   exact insets are tuned to the MEASURED card (161px, not the 172 the
   arithmetic suggested), because its text column has to hold the
   reference wording "Who is in the society" on one line. */
.qtile--tall .qtile__foot { padding: 10px 9px 11px 11px; gap: 7px; }

.qtile__b { flex: 1; min-width: 0; }
.qtile__t {
  display: block; font-size: 13.5px; font-weight: 800;
  letter-spacing: -.3px; line-height: 1.2; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.qtile--tall .qtile__t { font-size: 17px; letter-spacing: -.45px; }
.qtile__s {
  display: block; margin-top: 2px;
  font-size: 11px; font-weight: 500; line-height: 1.3; color: var(--text-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* 10.5px. "Who is in the society" — the reference's own wording — needs
   104px, and the tall card's measured text column is 100px once the go
   button and insets are taken out. At 11px it truncated to "Who is in the
   s...". Measured on the rendered page, not calculated from the design
   width: the card is 161px, not the 172 the arithmetic gave. */
.qtile--tall .qtile__s { font-size: 10.5px; margin-top: 3px; }

/* Go button. Filled on the tall cell, washed on the other two — one primary
   action per screen region, the same rule the CTA bars follow. It is
   decorative: the whole card is the link, so it is hidden from assistive
   tech rather than announced as a second control to the same place. */
.qtile__go {
  width: 30px; height: 30px; flex: none; border-radius: 10px;
  display: grid; place-items: center;
  background: var(--primary-wash); color: var(--primary-d);
}
.qtile__go svg { width: 16px; height: 16px; }
.qtile--tall .qtile__go { width: 32px; height: 32px; border-radius: 11px; background: var(--primary); color: #fff; }
.qtile--tall .qtile__go svg { width: 17px; height: 17px; }

/* Live count, sitting on the photo. Brand teal, not --error: red is this
   app's "act on me" tone (the nav badge uses it for entries awaiting a
   resident), and a count of people already inside is not something to act
   on. The white ring lifts it off whatever the photograph is doing. */
.qtile__pip {
  position: absolute; top: 10px; right: 10px; z-index: 1;
  min-width: 26px; height: 26px; padding: 0 8px; border-radius: var(--r-pill);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 12.5px; font-weight: 800; font-variant-numeric: tabular-nums;
  background: var(--primary); color: #fff;
  box-shadow: 0 0 0 2.5px rgba(255,255,255,.92);
}

/* Honour a system request for more contrast — the same screen in the same
   sun, but on a device whose owner has already said thin edges do not work
   for them. */
@media (prefers-contrast: more) {
  .qtile, .gstat { border-width: 2px; border-color: var(--text-3); }
  .qtile__s, .gstat__k { color: var(--text); }
}

/* ---- New Entry chips — SS/New Entry Sub Options.png --------------------- */
.vchips {
  display: flex; gap: 8px; margin-top: 12px; overflow-x: auto;
  scrollbar-width: none; padding-bottom: 2px;
}
.vchips::-webkit-scrollbar { display: none; }
.vchip {
  display: inline-flex; align-items: center; gap: 8px; flex: none;
  padding: 8px 14px 8px 8px; border-radius: 999px; cursor: pointer;
  background: var(--surface-2); border: 1.5px solid transparent;
  font-size: 12px; font-weight: 600; color: var(--text-2);
  text-decoration: none; transition: .15s var(--ease);
}
.vchip:active { transform: scale(.96); }
.vchip .vci {
  width: 28px; height: 28px; border-radius: 50%; background: var(--surface);
  display: grid; place-items: center; color: var(--text); flex: none;
  box-shadow: 0 1px 3px rgba(20,22,30,.08);
}
.vchip:hover { background: var(--primary-wash); border-color: var(--primary); color: var(--primary-d); }
.vchip:hover .vci { color: var(--primary); }

/* Duotone visitor-type glyphs ported from the resident app's VIS_ICONS —
   outline paths tint via currentColor (set by .vci above), the accent
   shape is this one rule. Self-contained: no gradient defs. */
.ic-a { fill: var(--ic-accent); }

/* ---- Auth — splash, login, OTP (auth.html only) ------------------------- */
.auth { position: relative; flex: 1; overflow: hidden; }
.au-screen {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  background: var(--bg); transform: translateX(100%);
  transition: transform .44s var(--ease); will-change: transform;
}
.au-screen.active { transform: translateX(0); }
.au-screen.past { transform: translateX(-26%); }
.au-scroll { flex: 1; overflow-y: auto; scrollbar-width: none; }
.au-scroll::-webkit-scrollbar { display: none; }

/* splash — a striking dusk shot of a modern residential tower (lit
   balconies against a saturated blue sky), replacing a flat, dim block
   photo that undersold Wave City as a premium society. Verified live via
   `curl -sI` before landing (see the visual-pass report for the full list
   of Unsplash ids checked).

   The wordmark that used to sit over the photo (.sp-mark, carrying
   .sp-word/.sp-sub) was removed in the 2026-08-19 follow-up pass, and with
   it the "approved exception" gradient scrim (.sp-photo::after) that only
   existed to keep that overlaid text legible — once there is no text over
   the photo, a legibility scrim has nothing left to justify it, so rather
   than keep a gradient with no job this app's flat, no-gradient direction
   (see the top of this file) now holds here without exception. The photo
   is left to read on its own; the white sheet below still carries the
   welcome copy, so the composition reads as photo-then-copy rather than a
   photo with an accidental gap where the wordmark was. */
.sp-photo {
  position: relative; flex: 1 1 auto; min-height: 300px;
  background: var(--dark) url("https://images.unsplash.com/photo-1567684014761-b65e2e59b9eb?w=900&h=760&fit=crop&q=72") center/cover no-repeat;
}
.sp-sheet {
  position: relative; z-index: 3; flex: none; margin-top: -34px;
  background: var(--surface); border-radius: 34px 34px 0 0;
  padding: var(--s-8) var(--s-6) calc(var(--safe-b) + var(--s-6));
  text-align: center;
}
.sp-h1 { font-size: 25px; font-weight: 800; letter-spacing: -.5px; color: var(--text); }
.sp-tag { font-size: 13.5px; font-weight: 500; color: var(--text-2); line-height: 1.55; margin-top: var(--s-2); }
.sp-note { font-size: 11.5px; font-weight: 600; color: var(--text-3); margin-top: var(--s-4); }

/* login */
.w-band {
  position: relative; height: 168px; flex: none;
  background: var(--dark) url("https://images.unsplash.com/photo-1567684014761-b65e2e59b9eb?w=820&h=440&fit=crop&q=72") center/cover no-repeat;
}
.w-body { padding: var(--s-6) var(--s-6) var(--s-8); }
.w-h1 { font-size: 25px; font-weight: 800; letter-spacing: -.6px; line-height: 1.15; color: var(--text); }
.w-h1 span { color: var(--primary-d); }
.w-sub { font-size: 13.5px; font-weight: 500; color: var(--text-2); line-height: 1.5; margin-top: var(--s-2); }
.w-flbl { display: block; font-size: 12px; font-weight: 700; color: var(--text-2); margin: var(--s-6) 2px var(--s-2); }
.w-phone { display: flex; gap: 9px; }
.w-cc {
  display: inline-flex; align-items: center; gap: 7px; flex: none;
  border: 1.5px solid var(--border); background: var(--surface);
  border-radius: 14px; padding: 0 14px; font-size: 15px; font-weight: 800; color: var(--text);
}
.w-cc .flag { width: 20px; height: 14px; border-radius: 3px; overflow: hidden; display: block; }
.w-input {
  flex: 1; min-width: 0; border: 1.5px solid var(--border); background: var(--surface);
  border-radius: 14px; padding: 15px 16px; font-family: inherit; font-size: 16px;
  font-weight: 700; letter-spacing: .5px; color: var(--text); outline: none;
  transition: border-color .15s, box-shadow .15s;
}
.w-phone:focus-within .w-input { border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-wash); }
.w-terms { display: flex; align-items: flex-start; gap: 11px; margin-top: var(--s-5); cursor: pointer; }
.w-check {
  width: 22px; height: 22px; border-radius: 7px; border: 1.9px solid var(--border);
  background: var(--surface); flex: none; display: grid; place-items: center;
  transition: .16s; margin-top: 1px;
}
.w-check svg { opacity: 0; transform: scale(.5); transition: .16s; }
.w-terms.on .w-check { background: var(--primary); border-color: var(--primary); }
.w-terms.on .w-check svg { opacity: 1; transform: scale(1); }
/* margin:0 is the actual fix here — the resident app resets every element's
   margin globally (`* { margin:0 }`); this file only zeroes html/body, so
   this bare <p> was carrying the UA default ~1em top margin, pushing its
   first line down against the checkbox's near-zero (1px) offset. */
.w-terms p { margin: 0; font-size: 12.5px; font-weight: 500; color: var(--text-2); line-height: 1.5; }
.w-terms a { color: var(--primary-d); font-weight: 800; text-decoration: none; }

.au-btn {
  width: 100%; border: none; border-radius: 16px; padding: 17px;
  font-family: inherit; font-size: 15.5px; font-weight: 800; letter-spacing: -.2px;
  background: var(--primary); color: #fff;
  box-shadow: 0 6px 16px -6px rgba(12,124,140,.45); cursor: pointer;
  margin-top: var(--s-5); display: inline-flex; align-items: center;
  justify-content: center; gap: 9px;
  transition: transform .12s var(--ease), opacity .15s, box-shadow .2s;
}
.au-btn:active { transform: scale(.985); }
.au-btn:disabled { opacity: .42; cursor: default; box-shadow: none; }
.au-back {
  position: absolute; left: 16px; top: 14px; z-index: 4;
  width: 40px; height: 40px; border-radius: 12px;
  background: rgba(255,255,255,.9); border: none; color: var(--text);
  display: grid; place-items: center; cursor: pointer; box-shadow: var(--sh-1);
}

/* ---- OTP screen -------------------------------------------------------- */
#auOtp { background: var(--bg); }
.o-hero {
  flex: none; position: relative; text-align: center; padding: 14px 24px 50px;
  background: linear-gradient(180deg, var(--bg-top) 0%, var(--bg) 100%);
}
.o-badge {
  width: 70px; height: 70px; margin: 26px auto 0; border-radius: 22px;
  background: var(--surface); border: 1px solid var(--divider);
  color: var(--text-2); display: grid; place-items: center; box-shadow: var(--sh-2);
}
.o-h1 { font-size: 23px; font-weight: 800; letter-spacing: -.4px; margin-top: 18px; color: var(--text); }
.o-hsub { font-size: 13px; font-weight: 500; color: var(--text-3); line-height: 1.55; margin-top: 8px; }
.o-hsub b { color: var(--text); font-weight: 800; }
.o-chg {
  display: inline-flex; align-items: center; gap: 6px; margin-top: 15px;
  background: var(--surface); border: 1px solid var(--border); color: var(--text-2);
  font-family: inherit; font-size: 12px; font-weight: 800; padding: 8px 15px;
  border-radius: 999px; cursor: pointer; box-shadow: var(--sh-1);
}
.o-card {
  position: relative; z-index: 2; margin: -30px 20px 0;
  background: var(--surface); border-radius: 24px;
  box-shadow: 0 16px 36px -16px rgba(20,22,30,.16), var(--sh-1);
  padding: 24px 20px 22px;
}
.o-clbl { text-align: center; font-size: 12px; font-weight: 800; color: var(--text-2); }
.otp-row { display: flex; gap: 9px; margin-top: 16px; }
.otp-box {
  flex: 1; min-width: 0; height: 60px; border: 1.25px solid var(--border);
  background: var(--surface); border-radius: 15px; text-align: center;
  font-family: inherit; font-size: 24px; font-weight: 800; color: var(--text);
  outline: none; font-variant-numeric: tabular-nums;
  transition: border-color .15s, box-shadow .15s, background .15s, color .15s, transform .12s;
}
.otp-box.filled { border-color: var(--text-2); }
.otp-box:focus { border-color: var(--text); box-shadow: 0 0 0 3px rgba(27,29,36,.09); transform: translateY(-1px); }
.otp-box.err { border-color: var(--error); background: var(--error-bg); color: var(--error-text); animation: otpShake .38s var(--ease); }
@keyframes otpShake {
  0%,100% { transform: translateX(0); } 22% { transform: translateX(-5px); }
  44% { transform: translateX(5px); } 66% { transform: translateX(-3px); } 88% { transform: translateX(3px); }
}
.o-error {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  margin-top: 14px; font-size: 12px; font-weight: 700; color: var(--error-text);
}
.o-error[hidden] { display: none; }
.o-demo {
  display: flex; align-items: center; justify-content: center; gap: 7px;
  margin: 16px auto 0; width: max-content; font-size: 11px; font-weight: 700;
  color: var(--text-2); background: var(--surface-2); border-radius: 999px; padding: 7px 13px;
}
.o-resend { text-align: center; font-size: 12px; font-weight: 600; color: var(--text-3); margin-top: 16px; }
.o-resend button {
  background: none; border: none; font-family: inherit; font-size: 12px;
  font-weight: 800; color: var(--primary-d); cursor: pointer; padding: 0;
}
.o-resend button:disabled { color: var(--text-3); cursor: default; }
