/* Liquid & Larder — styles shared by staff pages only. Load after core.css. */
/* v3: light paper theme only (dark theme retired with the v3 refresh). */
/* v7 (30 Jul 2026): colour tokens. No raw hex or rgba() below this point — the
   staff-only tokens live in the :root block that follows; the base palette lives in
   core.css v4. See the HOUSE RULE at the top of core.css. */

:root {
  /* --- chart / data-viz palette (metrics + Quality) --------------------- */
  --chart-clay:#b08968;       /* warm tan series: forecast line, 1-month return, weekday bars */
  --chart-clay-deep:#9a7350;  /* second stop of the weekend bar gradient */
  --chart-stone:#9a8f7a;      /* muted stone series: totals line, F bars, secondary axis labels */
  --chart-nodata:#cccccc;     /* a bar or point with no data behind it */
  --err-soft:#c08a6e;         /* first stop of the low-score gradient, runs into --err */
  --dim-soft:#8b938e;         /* the year sub-label under a chart axis (v8: was #9aa19d, 2.64:1 on --card — now 3.15:1) */
  --dot-off:#cfc8bb;          /* an inactive chip's dot */

  /* --- gold / warn family ---------------------------------------------- */
  --gold:#9a6b2f; --gold-rgb:154 107 47; --gold-ink:#8a5a24;

  /* --- VIP tier chips --------------------------------------------------- */
  --tier-plat-bg:#e6e8ec;   --tier-plat-ink:#5b6472;   --tier-plat-bd:#cfd4dc;
  --tier-gold-bg:#f6e9c6;   --tier-gold-ink:#7d5f0e;   --tier-gold-bd:#e6d29a;  /* v8: ink was #8a6a12, 4.19:1 — now 4.95:1 */
  --tier-silver-bg:#e9edf1; --tier-silver-ink:#5c6b78; --tier-silver-bd:#d3dbe2;
  --tier-bronze-bg:#f0e0d0; --tier-bronze-ink:#8a5a2c; --tier-bronze-bd:#e0c8ad;
}

/* ==========================================================================
   v8 (30 Jul 2026) — dark theme, staff layer. The base palette's dark block
   lives in core.css v5; this is the staff-only half. Two light values were
   also corrected here for contrast, marked inline above.
   ========================================================================== */
:root[data-theme="dark"] {
  /* --- chart / data-viz palette ----------------------------------------- */
  --chart-clay:#dba475; --chart-clay-deep:#ba8551; --chart-stone:#b5aa93;
  --chart-nodata:#3f4f4a; --err-soft:#daa082; --dim-soft:#78897f; --dot-off:#46564f;

  /* --- gold / warn family ---------------------------------------------- */
  --gold:#dbae5f; --gold-rgb:219 174 95; --gold-ink:#e9c68a;

  /* --- VIP tier chips --------------------------------------------------- */
  --tier-plat-bg:#26313a;   --tier-plat-ink:#c4cdd9;   --tier-plat-bd:#374551;
  --tier-gold-bg:#2e2f1d;   --tier-gold-ink:#e6ca8e;   --tier-gold-bd:#434427;
  --tier-silver-bg:#232f33; --tier-silver-ink:#bdccd7; --tier-silver-bd:#35434a;
  --tier-bronze-bg:#2f2a1f; --tier-bronze-ink:#e1b690; --tier-bronze-bd:#45392b;

  /* --- the primary data series, lifted so it reads on a dark card (7.4:1).
         core.css sets the light value; this is the dark one. --------------- */
  --chart-accent:#82bfae;
}
body { padding:0 0 60px; }
/* The bar is position:sticky, so once the page is scrolled it pins to the
   VIEWPORT's top - which in a standalone launch is underneath the clock and the
   battery, whatever padding <html> carries. The inset therefore belongs to the
   bar itself: its blurred background extends up behind the status bar and its
   content starts below it. env() resolves to 0 in an ordinary tab, so this is a
   no-op everywhere else. core.css drops its own html padding-top when a topbar
   is present, so the two never double up. */
.topbar { position:sticky; top:0; z-index:40; backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px);
  background:rgb(var(--bg-rgb) / .75); border-bottom:1px solid var(--hairline); margin-bottom:38px;
  padding-top:env(safe-area-inset-top); }
.topbar .bar { max-width:var(--page-w,1004px); margin:0 auto; padding:16px 22px;
  display:flex; justify-content:space-between; align-items:center; gap:14px; }
.wrap { max-width:var(--page-w,1004px); margin:0 auto; padding:0 22px; animation:rise .5s ease both; }
@keyframes rise { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:none; } }
h1.logo { width:172px; height:20px; background:var(--accent-ink);
  -webkit-mask:url(/assets/wordmark.svg) no-repeat left center / contain;
  mask:url(/assets/wordmark.svg) no-repeat left center / contain; }
.sub { color:var(--dim); font-size:10px; letter-spacing:.24em; text-transform:uppercase; margin-top:5px; }
.card { padding:28px; margin-bottom:26px; }
.cardTitle { font-family:'Gilda Display',serif; font-size:19px; margin-bottom:20px; color:var(--accent-ink); }
/* The bare-element trap, twin of core.css's `button` rule: EVERY <label> in the
   hub shouts by default and each one has to opt out by hand. The uppercase is
   right for a micro-label above a field, which is what this rule was written
   for, so it stays - but two consequences of it were never intended and are
   corrected below.  (15 Aug 2026) */
label { display:block; font-size:10px; text-transform:uppercase; letter-spacing:.16em; color:var(--dim);
  margin-bottom:6px; font-weight:600; }
/* 1. text-transform INHERITS, so a value the person typed into a field wrapped
      in a label came back at them in capitals. */
label input, label select, label textarea {
  text-transform:none; letter-spacing:normal; }
/* 2. A label wrapping a tick box is a SENTENCE, not a group heading - it reads
      "Include me in birthday and work anniversary mentions", and in capitals it
      shouted at people. :has() is Safari 15.4+ / Chrome 105+; where it is not
      supported nothing changes, which is today's behaviour. */
label:has(> input[type="checkbox"]), label:has(> input[type="radio"]) {
  font-size:13px; text-transform:none; letter-spacing:.01em; color:var(--ink); font-weight:400; }
input, select { width:100%; padding:11px 13px; background:var(--surface); color:var(--ink);
  border:1px solid var(--border); border-radius:var(--r-input); font-size:13.5px; font-family:'Montserrat',sans-serif;
  transition:border-color .15s, box-shadow .15s; }
input:focus, select:focus { outline:none; border-color:var(--accent-ink); box-shadow:var(--ring); background:var(--card); }
button { padding:12px 26px; font-size:11px; }
/* .ghost overrode background, colour, padding and size but NOT text-transform,
   so every topbar ghost in the hub read COPY LINK and REFRESH. A ghost is a
   control, and controls take sentence case - see the house rule in core.css. */
button.ghost { background:var(--sand-soft); color:var(--accent-ink); box-shadow:none; padding:7px 16px;
  font-size:11px; letter-spacing:.01em; text-transform:none; }
button.ghost:hover { transform:none; filter:none; background:var(--sand-hi); }
button.danger { background:rgb(var(--err-rgb) / .10); color:var(--err); box-shadow:none; padding:7px 16px;
  font-size:11px; letter-spacing:.01em; text-transform:none; }
button.danger:hover { transform:none; filter:none; background:rgb(var(--err-rgb) / .16); }
.msg { margin-top:12px; }
table { width:100%; border-collapse:collapse; font-size:13px; }
th { text-align:left; color:var(--dim); font-size:10px; text-transform:uppercase; letter-spacing:.16em;
  padding:10px; border-bottom:1px solid var(--hairline); font-weight:600; }
td { border-bottom:1px solid var(--hairline); vertical-align:middle; }
tbody tr { transition:background .12s; }
tbody tr:hover { background:var(--surface-hover); }
tbody tr:last-child td { border-bottom:none; }
.dim { color:var(--dim); font-size:11px; }

/* v4: shared prominent back-link pill, used across app pages (quality, preauth, finance, metrics) */
a.back { display:inline-flex; align-items:center; gap:4px; font-size:11px; font-weight:700; color:var(--accent-ink); text-decoration:none; background:var(--sand-soft); padding:8px 15px; border-radius:999px; letter-spacing:.04em; transition:background .15s; }
a.back:hover { background:var(--sand-hi); }

/* v5: shared responsive rules — keep every staff page inside the phone viewport.
   Wide tables sit in an overflow-x:auto wrapper so they scroll within their card
   instead of pushing the whole page wider than the screen. */
table { max-width:100%; }
@media (max-width:640px) {
  .topbar { margin-bottom:26px; }
  .topbar .bar { padding:13px 14px; gap:8px; }
  .wrap { padding:0 16px; }
  .card { padding:22px 18px; }
  /* the wordmark is a fixed-width mask; let it shrink so the header fits narrow phones */
  h1.logo { width:132px; height:16px; }
}
@media (max-width:400px) {
  h1.logo { width:108px; }
  .topbar .who .avatar { display:none; }
}

/* ==========================================================================
   v6 — motion layer (28 Jul 2026)
   Shared micro-interactions and page transitions for every staff page.
   Rules of the house:
     · animate transform/opacity only — compositor work, never layout
     · 130ms feedback / 220ms movement / 380ms entrances
     · new utility classes are prefixed .ll- so they cannot collide with a
       page's own inline <style> block
     · the whole layer is switched off by prefers-reduced-motion at the foot
   Paired with /scripts/hub-motion.js (count-up, spotlight, toasts).
   ========================================================================== */

:root {
  --e-out:cubic-bezier(.22,.9,.28,1);      /* entering */
  --e-spring:cubic-bezier(.34,1.4,.5,1);   /* things that should feel physical */
  --t-fast:130ms; --t-med:220ms; --t-slow:380ms;
}

/* --- 1. cross-document page transitions ---------------------------------
   Chrome/Edge cross-fade page to page; Safari/Firefox ignore this and keep
   today's hard cut, so there is nothing to fall back to. Guest pages don't
   load hub.css, so they are deliberately excluded. */
@view-transition { navigation: auto; }
::view-transition-old(root) { animation:llFadeOut 140ms ease both; }
::view-transition-new(root) { animation:llPageIn 240ms var(--e-out) both; }
@keyframes llFadeOut { to { opacity:0; } }
@keyframes llPageIn { from { opacity:0; transform:translateY(8px); } }

/* --- 2. staggered entrances ---------------------------------------------
   Tiles and KPI cards are injected once their query resolves, so these fire
   as the data lands rather than on page load. */
@keyframes llRise { from { opacity:0; transform:translateY(12px); } to { opacity:1; transform:none; } }
.tiles > a.tile { animation:llRise .5s var(--e-out) both; }
.tiles > a.tile:nth-child(1)  { animation-delay:0ms; }
.tiles > a.tile:nth-child(2)  { animation-delay:55ms; }
.tiles > a.tile:nth-child(3)  { animation-delay:110ms; }
.tiles > a.tile:nth-child(4)  { animation-delay:165ms; }
.tiles > a.tile:nth-child(5)  { animation-delay:220ms; }
.tiles > a.tile:nth-child(6)  { animation-delay:275ms; }
.tiles > a.tile:nth-child(7)  { animation-delay:330ms; }
.tiles > a.tile:nth-child(8)  { animation-delay:385ms; }
.tiles > a.tile:nth-child(n+9){ animation-delay:440ms; }

.kpis > .kpi { animation:llRise .42s var(--e-out) both; }
.kpis > .kpi:nth-child(1)  { animation-delay:0ms; }
.kpis > .kpi:nth-child(2)  { animation-delay:45ms; }
.kpis > .kpi:nth-child(3)  { animation-delay:90ms; }
.kpis > .kpi:nth-child(4)  { animation-delay:135ms; }
.kpis > .kpi:nth-child(5)  { animation-delay:180ms; }
.kpis > .kpi:nth-child(n+6){ animation-delay:225ms; }

/* --- 3. hover life ------------------------------------------------------ */
.appIcon { transition:transform var(--t-med) var(--e-spring), background var(--t-med); }
a.tile:hover .appIcon { transform:scale(1.06) rotate(-3deg); background:var(--sand-hi); }
a.tile.soon:hover .appIcon { transform:none; background:var(--sand-soft); }
a.navItem svg { transition:transform var(--t-med) var(--e-spring), stroke var(--t-fast); }
a.navItem:hover svg { transform:scale(1.12) rotate(-4deg); }
a.back { transition:background var(--t-fast), transform var(--t-fast) var(--e-out); }
a.back:hover { transform:translateX(-2px); }

/* --- 4. cursor spotlight on tiles ---------------------------------------
   hub-motion.js writes --ll-mx / --ll-my; without the script the tile simply
   never lights up, so this degrades to today's behaviour. */
a.tile::after {
  content:''; position:absolute; inset:0; z-index:0; pointer-events:none; opacity:0;
  border-radius:inherit; transition:opacity var(--t-med);
  background:radial-gradient(340px circle at var(--ll-mx,50%) var(--ll-my,50%),
                             rgb(var(--sand-rgb) / .42), transparent 62%);
}
a.tile:hover::after { opacity:1; }
a.tile.soon::after { display:none; }
a.tile > * { position:relative; z-index:1; }

/* --- 5. buttons --------------------------------------------------------- */
button:active { transform:translateY(0) scale(.97); transition-duration:60ms; }
button:disabled:active { transform:none; }

button.ll-busy { color:transparent !important; position:relative; pointer-events:none; }
button.ll-busy .ll-spin { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; }
button.ll-busy .ll-spin i { display:block; width:14px; height:14px; border-radius:50%;
  border:2px solid rgb(var(--card-rgb) / .28); border-top-color:var(--on-accent); animation:llSpin .7s linear infinite; }
button.ghost.ll-busy .ll-spin i { border-color:rgb(var(--tint-rgb) / .22); border-top-color:var(--accent-ink); }
@keyframes llSpin { to { transform:rotate(360deg); } }
button.ll-done { color:transparent !important; background:var(--ok) !important; position:relative; }
button.ll-done .ll-tick { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; }
button.ll-done .ll-tick svg { width:16px; height:16px; stroke:var(--on-accent); stroke-width:2.6; fill:none;
  stroke-linecap:round; stroke-linejoin:round; stroke-dasharray:26; stroke-dashoffset:26;
  animation:llDraw .32s var(--e-out) .05s forwards; }
@keyframes llDraw { to { stroke-dashoffset:0; } }

/* --- 6. table rows ------------------------------------------------------
   A 2px anchor that wipes in from the centre. Excludes the Team admin
   permission matrix, whose first column is sticky. */
table:not(.matrix) tbody tr td:first-child { position:relative; }
table:not(.matrix) tbody tr td:first-child::before {
  content:''; position:absolute; left:0; top:6px; bottom:6px; width:2px; border-radius:2px;
  background:var(--accent-ink); transform:scaleY(0); transform-origin:center;
  transition:transform var(--t-med) var(--e-out);
}
table:not(.matrix) tbody tr:hover td:first-child::before { transform:scaleY(1); }

/* --- 7. keyboard focus -------------------------------------------------- */
button:focus-visible, a:focus-visible, summary:focus-visible, [tabindex]:focus-visible {
  outline:none; box-shadow:var(--ring); animation:llBloom .45s var(--e-out);
}
@keyframes llBloom { from { box-shadow:0 0 0 0 rgb(var(--green-rgb) / .34); } to { box-shadow:var(--ring); } }

/* --- 8. live status dot ------------------------------------------------- */
.dot.live { animation:llBreathe 2.6s var(--e-out) infinite; }
@keyframes llBreathe { 0%,100% { box-shadow:0 0 0 0 rgb(var(--ok-rgb) / .38); } 50% { box-shadow:0 0 0 6px rgb(var(--ok-rgb) / 0); } }

/* --- 9. value-change flash (added by hub-motion.js) --------------------- */
.ll-flash { animation:llFlash .95s var(--e-out); border-radius:6px; }
@keyframes llFlash { 0% { background:rgb(var(--ok-rgb) / .20); } 100% { background:transparent; } }

/* --- 10. sparkline draw-on ----------------------------------------------
   Covers both shapes in use: <path stroke> on the home tiles and <polyline
   stroke> on the Quality venue cards. Anything that already carries its own
   stroke-dasharray (the dashed pass-mark line) is left alone — overriding it
   would turn that line solid. */
svg.spark path[stroke]:not([stroke-dasharray]),
svg.spark polyline[stroke]:not([stroke-dasharray]) {
  stroke-dasharray:var(--ll-len,140); stroke-dashoffset:var(--ll-len,140);
  animation:llDrawSpark 1s var(--e-out) .1s forwards; }
@keyframes llDrawSpark { to { stroke-dashoffset:0; } }
svg.spark path:not([stroke]) { opacity:0; animation:llAppear .5s ease .55s forwards; }
@keyframes llAppear { to { opacity:1; } }

/* --- 11. skeleton placeholders ------------------------------------------ */
.ll-sk { border-radius:8px; background:linear-gradient(90deg,var(--rule) 25%,var(--rule-hi) 37%,var(--rule) 63%);
  background-size:400% 100%; animation:llShim 1.35s ease infinite; }
@keyframes llShim { 0% { background-position:100% 0; } 100% { background-position:-100% 0; } }
.ll-skRow { display:flex; align-items:center; gap:13px; padding:11px 0; border-bottom:1px solid var(--hairline); }

/* --- 12. toasts (window.LL.toast) --------------------------------------- */
#llToastZone { position:fixed; right:22px; bottom:22px; z-index:200;
  display:flex; flex-direction:column; gap:10px; align-items:flex-end; pointer-events:none; }
.ll-toast { pointer-events:auto; display:flex; align-items:center; gap:11px;
  background:var(--card); border:1px solid var(--hairline); border-left:3px solid var(--ok);
  border-radius:12px; box-shadow:0 8px 30px -6px rgb(var(--green-rgb) / .22); padding:13px 18px;
  font-size:12.5px; color:var(--ink); min-width:230px; max-width:min(360px,80vw);
  animation:llToastIn .42s var(--e-spring) both; }
.ll-toast.err { border-left-color:var(--err); }
/* pages define their own .dot sizing; the toast must not depend on that */
.ll-toast .dot { width:7px; height:7px; border-radius:50%; flex:0 0 7px; background:var(--ok); }
.ll-toast.err .dot { background:var(--err); }
.ll-toast.out { animation:llToastOut .28s var(--e-out) both; }
@keyframes llToastIn { from { opacity:0; transform:translateY(14px) scale(.96); } to { opacity:1; transform:none; } }
@keyframes llToastOut { to { opacity:0; transform:translateX(24px); } }
@media (max-width:560px) {
  #llToastZone { right:14px; left:14px; bottom:14px; align-items:stretch; }
  .ll-toast { min-width:0; max-width:none; }
}

/* --- 13. a shadow under the topbar once you've scrolled ----------------- */
@supports (animation-timeline: scroll()) {
  .topbar { animation:llStuck linear both; animation-timeline:scroll(root); animation-range:0 70px; }
  @keyframes llStuck { to { box-shadow:0 6px 22px -16px rgb(var(--green-rgb) / .55); } }
}

/* --- 14. beacon ring -----------------------------------------------------
   A fine gold light that travels around the outline of The Pass's hero card on
   Home. Gold rather than green so it reads as "look here" and never competes
   with the green active-page highlight in the sidebar.

   Home only, deliberately. It was tried on the sidebar item too and pulled the
   eye every time you glanced at the nav, on every page — a beacon you cannot
   get away from stops being a beacon. One place, where you land.

   It is ALWAYS on by design — a deliberate nudge, not an unread signal. The
   unread count is worked out on The Pass page itself and no other page knows
   it; if this should ever go quiet once you're up to date, that needs an RPC
   the page can call at boot.

   NOTHING SPILLS OUTSIDE THE OUTLINE. The travelling segment is a conic
   gradient masked down to the border itself, so unlike a glow or a growing
   halo it cannot tint whatever sits beside it.

   Two layers: ::before is a permanent hairline ring so the shape always reads,
   ::after is the bright segment sweeping around it. The mask-composite pair
   below cuts the middle out of a filled box and leaves the border; both the
   standard and -webkit- properties are needed for Safari.

   Browsers without @property support cannot animate an angle, so there the
   segment simply sits still — the hairline ring still does its job. The same
   is true under the reduced-motion escape hatch below, which is why the ring
   is a separate always-on layer rather than a keyframe. */
@property --llBeaconAngle { syntax:'<angle>'; initial-value:0deg; inherits:false; }
@keyframes llBeaconSweep { to { --llBeaconAngle:360deg; } }

/* The ring goes on the WRAPPER, not the card: `a.tile` is overflow:hidden, so
   a ring on the card itself would be clipped away at exactly the edge it is
   meant to trace. index.html adds `.beacon` here only when there is a card. */
#passHero.beacon { position:relative; }
#passHero.beacon::before, #passHero.beacon::after {
  content:''; position:absolute; inset:-3px; border-radius:calc(var(--r-card) + 3px);
  padding:1.5px;                                   /* = the ring thickness */
  pointer-events:none;
  -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
          mask:linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
}
#passHero.beacon::before { background:rgb(var(--gold-rgb) / .32); }
#passHero.beacon::after {
  background:conic-gradient(from var(--llBeaconAngle),
    rgb(var(--gold-rgb) / 0) 0 58%,
    rgb(var(--gold-rgb) / .25) 74%,
    var(--gold) 88%,
    rgb(var(--gold-rgb) / .25) 95%,
    rgb(var(--gold-rgb) / 0) 100%);
  animation:llBeaconSweep 4.5s linear infinite;
}

/* --- 15. the escape hatch ----------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration:.001ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.001ms !important;
    scroll-behavior:auto !important;
  }
}

/* ==========================================================================
   v8 — the theme control (System / Light / Dark)
   hub-motion.js injects this into each page's topbar, so no page needs markup.
   Prefixed .ll- like the rest of the shared layer, and built from tokens only.
   ========================================================================== */
.ll-theme { display:inline-flex; align-items:center; gap:2px; flex:none;
  background:var(--sand-soft); border-radius:999px; padding:3px; }
.ll-theme button { border:none; background:transparent; box-shadow:none; cursor:pointer;
  width:28px; height:28px; padding:0; border-radius:999px; display:flex; align-items:center;
  justify-content:center; color:var(--dim); transition:background var(--t-fast), color var(--t-fast); }
.ll-theme button svg { width:15px; height:15px; fill:none; stroke:currentColor;
  stroke-width:1.8; stroke-linecap:round; stroke-linejoin:round; }
.ll-theme button:hover { transform:none; filter:none; background:var(--sand-hi); color:var(--accent-ink); }
.ll-theme button[aria-pressed="true"] { background:var(--card); color:var(--accent-ink);
  box-shadow:0 1px 3px rgb(var(--green-rgb) / .12); }
:root[data-theme="dark"] .ll-theme button[aria-pressed="true"] { box-shadow:0 1px 3px rgb(0 0 0 / .35); }
/* The home page's sidebar footer is one tight flex row — avatar, name, sign-out.
   Squeezing the control in beside them truncated the name to an ellipsis and
   wrapped the role onto two lines, so give it a line of its own underneath.
   .sideUser sets no flex-wrap of its own, so this wins without a specificity fight. */
.sideUser { flex-wrap:wrap; }
.sideUser .ll-theme { flex:0 0 100%; margin-left:0; margin-top:12px; justify-content:center; }
@media (max-width:400px) { .ll-theme button { width:26px; height:26px; } }

/* ==========================================================================
   v9 — the shared mobile layer (8 Aug 2026)

   Everything here exists because a phone is not a small laptop. Four rules,
   each fixing a defect that was live across every page:

     1. iOS Safari ZOOMS the viewport whenever a focused field is under 16px,
        and does not zoom back out. Every field in the hub was 12–13.5px.
     2. Touch devices have no hover, so hover-only affordances are invisible
        and a tapped element keeps its hover state until you tap elsewhere.
     3. Controls were as small as 17px against a 44px minimum.
     4. `100vh` on a mobile browser excludes the address bar, so full-height
        panels run underneath it.

   Kept in hub.css rather than each page so a new app inherits the lot.
   ========================================================================== */

/* ---- 1. no zoom-on-focus ------------------------------------------------
   !important is deliberate and load-bearing. Page-level <style> blocks come
   after this file and set their own field sizes (`select.mini` at 12px,
   `input.find` at 13px), so a normal declaration here loses every time — the
   same cascade trap that silently defeated the safe-area rule in July. This
   is the one rule in the hub that may not lose. Visual weight comes back
   through padding and letter-spacing, not through shrinking the type. */
@media (max-width:768px) {
  input:not([type="checkbox"]):not([type="radio"]),
  select,
  textarea { font-size:16px !important; }
}

/* ---- 2. hover belongs to pointers ---------------------------------------
   Neutralises the lift/filter artefacts of a hover state that a touch device
   should never have been given. Deliberately narrow: colour and background
   hovers are harmless, it is movement that reads as a stuck state. */
@media (hover:none) {
  a:hover, button:hover, .tile:hover, .navItem:hover, .chips button:hover {
    transform:none;
    filter:none;
  }
}

/* ---- 3. 44px minimum touch target ---------------------------------------
   Applied by height only, so nothing changes width and no layout reflows.
   `.tapSmall` opts a control out — for genuine miniatures like the remove
   cross on a recipient pill, which instead earns its hit area from the
   invisible ::after below. */
@media (max-width:768px) {
  button:not(.tapSmall),
  select,
  input:not([type="checkbox"]):not([type="radio"]),
  a.navItem,
  a.back,
  .tabbar a,
  [role="button"]:not(.tapSmall) { min-height:44px; }

  /* a.back is the "← Hub" pill on every app page — the most-tapped control in
     the hub, and it was 28px. It is inline-flex, so a min-height alone leaves
     the label off-centre; re-centre it rather than adding vertical padding,
     which would push the topbar taller on every page at once. */
  a.back { align-items:center; }

  /* Miniature controls keep their size and gain a 44px invisible hit area.
     The parent needs position:relative for this to anchor, which every
     current user of .tapSmall already has. */
  .tapSmall { position:relative; }
  .tapSmall::after {
    content:'';
    position:absolute;
    top:50%; left:50%;
    width:44px; height:44px;
    transform:translate(-50%,-50%);
  }
}

/* ---- 4. dynamic viewport height ------------------------------------------
   A page opts in with .fullHeight rather than repeating the pair. The bare
   100vh line stays as the fallback for anything that predates dvh. */
.fullHeight { min-height:100vh; min-height:100dvh; }
