/* ===========================================================================
   kiosk.css — I Solution Pakistan self-service kiosk.

   Premium, confident, enterprise. A living brand field (drifting colour glows,
   a moving dot grid, a quiet circuit) behind white cards — frozen on the kiosk
   that all share ONE icon container: identical size, radius, fill, border and
   shadow on every service, with only the glyph changing. Plus Jakarta Sans for
   every heading; one constant optical stroke weight across the icon family.
   Tablet-first: composed for a 10-inch 1280×800 landscape screen.

   Layout is keyed on data-LAYOUT (viewport), never data-surface (kiosk key).
   The key governs privacy only — see the note above the type scale in
   tokens.css.

   No hex values. No --brand-* tokens in components. Semantic tokens only.
   =========================================================================== */

/* --- Reset ------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; height: 100%; overflow-x: hidden; }
button { font: inherit; color: inherit; border: 0; background: none; cursor: pointer; }
img, svg { display: block; max-width: 100%; }
:focus { outline: none; }
:focus-visible { box-shadow: var(--focus-ring); border-radius: var(--radius-md); }

body {
  font-family: var(--font-ui);
  font-size: var(--fs-body);
  line-height: var(--lh-latin);
  color: var(--ink);
  background: var(--bg-field);
  background-attachment: fixed;
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: transparent;
}

/* =======================================================================
   AMBIENT ENVIRONMENT — the animated brand field
   Slow, low-opacity, GPU-only (transform/opacity). Fixed behind the app so it
   enriches the welcome most and stays a quiet backdrop elsewhere. Every
   colour comes from a semantic --env-* token. Killed by prefers-reduced-motion.
   ======================================================================= */
.page-bg { position: fixed; inset: 0; z-index: -1; overflow: hidden; pointer-events: none; }

/* Drifting colour glows — the logo's spectrum, breathing across the corners. */
.bg-orb { position: absolute; border-radius: 50%; will-change: transform; }
.bg-orb--cyan    { width: 48vw; height: 48vw; left: -9vw;  top: -11vw;   background: var(--env-orb-cyan);
  animation: orbDriftA var(--dur-orb) var(--ease) infinite alternate; }
.bg-orb--violet  { width: 44vw; height: 44vw; right: -7vw; top: -9vw;    background: var(--env-orb-violet);
  animation: orbDriftB calc(var(--dur-orb) - 4s) var(--ease) infinite alternate; }
.bg-orb--magenta { width: 42vw; height: 42vw; right: -5vw; bottom: -13vw; background: var(--env-orb-magenta);
  animation: orbDriftA calc(var(--dur-orb) + 6s) var(--ease) infinite alternate-reverse; }
.bg-orb--teal    { width: 46vw; height: 46vw; left: -7vw;  bottom: -15vw; background: var(--env-orb-teal);
  animation: orbDriftB var(--dur-orb) var(--ease) infinite alternate-reverse; }
@keyframes orbDriftA { from { transform: translate3d(0,0,0) scale(1); } to { transform: translate3d(4vw,3vh,0) scale(1.12); } }
@keyframes orbDriftB { from { transform: translate3d(0,0,0) scale(1.06); } to { transform: translate3d(-3vw,-3vh,0) scale(1); } }

/* Faint moving dot grid — a technology texture, edge-masked so it never
   reaches the borders. Translating exactly one tile loops seamlessly. */
.bg-grid {
  position: absolute; inset: -60px;
  background-image: radial-gradient(var(--env-dot) 1.4px, transparent 1.6px);
  background-size: 44px 44px;
  -webkit-mask-image: radial-gradient(125% 120% at 50% 42%, var(--mask-opaque) 52%, transparent 100%);
          mask-image: radial-gradient(125% 120% at 50% 42%, var(--mask-opaque) 52%, transparent 100%);
  will-change: transform;
  animation: gridDrift var(--dur-grid) linear infinite;
}
@keyframes gridDrift { from { transform: translate3d(0,0,0); } to { transform: translate3d(44px,44px,0); } }

/* Quiet circuit — thin connection lines and nodes that slowly pulse. */
.bg-circuit { position: absolute; inset: 0; width: 100%; height: 100%; opacity: .55; }
.bg-circuit__lines path { fill: none; stroke: var(--env-line); stroke-width: 1.5; stroke-linecap: round; }
.bg-circuit__nodes circle { fill: var(--env-node); opacity: .12; animation: nodePulse var(--dur-pulse) ease-in-out infinite; }
.bg-circuit__nodes circle:nth-child(2n) { animation-delay: 1.3s; }
.bg-circuit__nodes circle:nth-child(3n) { animation-delay: 2.5s; }
.bg-circuit__nodes circle:nth-child(4n) { animation-delay: .7s; }
/* Was .12 -> .85: a seven-fold opacity swing on ten nodes at once, which read
   as blinking rather than as a quiet circuit. The range is now narrow enough
   to suggest life without any element visibly flashing. */
@keyframes nodePulse { 0%, 100% { opacity: .12; } 50% { opacity: .24; } }

/* THE LIGHT SWEEP IS GONE, PERMANENTLY. DO NOT REINTRODUCE IT.
   It was a full-viewport layer (160% x 160%) compositing with
   mix-blend-mode: screen and ramping opacity 0 -> .55 -> 0 on a loop, which
   brightened the ENTIRE screen and back roughly every fifteen seconds. On the
   counter tablet it was the primary cause of the P0 blinking: proven by
   deploying a build with the ambient layer disabled and confirming on the real
   Lenovo Tab A10 that the blinking stopped completely.

   A screen-wide brightness pulse is wrong for this product independently of
   that bug. The tablet runs twelve hours a day on a shop counter; a display
   that visibly breathes is a display customers and staff learn to distrust,
   and it costs power and panel life for decoration nobody asked for.

   mix-blend-mode is banned here for the same reason: it forces the compositor
   to re-blend everything beneath it every frame, which an entry-level tablet
   GPU cannot sustain alongside the rest of this layer. */

/* A handful of floating light particles. */
.bg-particles { position: absolute; inset: 0; }
.bg-particles span { position: absolute; width: 8px; height: 8px; border-radius: 50%;
  background: var(--env-particle); opacity: 0; will-change: transform, opacity;
  animation: floatUp var(--dur-float) ease-in-out infinite; }
@keyframes floatUp {
  0% { transform: translate3d(0,20px,0); opacity: 0; }
  15% { opacity: .85; }
  85% { opacity: .85; }
  100% { transform: translate3d(26px,-130px,0); opacity: 0; }
}
.bg-particles span:nth-child(1) { left: 11%; bottom: 16%; width: 7px;  animation-duration: 17s; animation-delay: 0s; }
.bg-particles span:nth-child(2) { left: 24%; bottom: 30%; width: 5px;  animation-duration: 22s; animation-delay: 3s; }
.bg-particles span:nth-child(3) { left: 38%; bottom: 12%; width: 9px;  animation-duration: 19s; animation-delay: 6s; }
.bg-particles span:nth-child(4) { left: 52%; bottom: 22%; width: 6px;  animation-duration: 24s; animation-delay: 1.5s; }
.bg-particles span:nth-child(5) { left: 63%; bottom: 34%; width: 8px;  animation-duration: 20s; animation-delay: 8s; }
.bg-particles span:nth-child(6) { left: 74%; bottom: 14%; width: 5px;  animation-duration: 23s; animation-delay: 4.5s; }
.bg-particles span:nth-child(7) { left: 84%; bottom: 28%; width: 7px;  animation-duration: 18s; animation-delay: 10s; }
.bg-particles span:nth-child(8) { left: 46%; bottom: 40%; width: 6px;  animation-duration: 26s; animation-delay: 2.5s; }
.bg-particles span:nth-child(9) { left: 92%; bottom: 40%; width: 5px;  animation-duration: 21s; animation-delay: 7s; }

/* --- THE COUNTER TABLET GETS A COMPLETELY STILL SCREEN ------------------
   The ambient field above is composed for a phone held for thirty seconds.
   The kiosk is a fixed installation that runs all day, and its hardware is an
   entry-level Lenovo Tab A10 whose GPU cannot sustain a dozen simultaneously
   animating composited layers. Under load it drops frames, and dropped frames
   on a decorative layer look exactly like a fault in the product.

   So on the kiosk the composition is kept and the motion is removed. The
   orbs, the grid and the nodes are all still there, in position, at full
   fidelity — they simply do not move. Nothing about the brand is lost; a
   customer looking at the counter screen was never going to watch a
   forty-four second grid drift anyway.

   will-change is also released. It exists to warn the compositor that a
   property is about to change; on a frozen element it only pins an extra GPU
   layer for no benefit, which on this device is memory that the QR rendering
   needs more. */
:root[data-layout="kiosk"] .bg-orb,
:root[data-layout="kiosk"] .bg-grid,
:root[data-layout="kiosk"] .bg-circuit__nodes circle,
:root[data-layout="kiosk"] .bg-particles span,
:root[data-layout="kiosk"] .welcome__logo {
  animation: none !important;
  will-change: auto;
}

/* The particles only exist as motion — frozen they are nine stray dots. */
:root[data-layout="kiosk"] .bg-particles { display: none; }

/* Both bars sit above the ambient layer and blurred what was behind them
   every frame. A backdrop-filter over a full-screen background is one of the
   most expensive composites available, and on the kiosk it buys nothing: the
   bars are opaque enough that no one can tell. --bar-solid is the same
   surface without the transparency that made the blur necessary. */
:root[data-layout="kiosk"] .appbar,
:root[data-layout="kiosk"] .chrome {
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
  background: var(--bar-solid);
}

/* --- App shell --------------------------------------------------------- */
#app { position: relative; z-index: 1; min-height: 100vh; min-height: 100dvh; display: flex; flex-direction: column; }
:root[data-layout="kiosk"] #app { height: 100vh; height: 100dvh; overflow: hidden; }

.screen {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column;
  padding: var(--screen-pad); gap: var(--stack-gap);
}
/* No overflow-y here. It used to say `overflow-y: auto`, which never scrolled
   anything: on the phone #app has min-height rather than height, so .screen
   grows to its content and the BODY is the scroller — measured scrollHeight
   equal to clientHeight on every screen.

   It was not harmless, though. An `auto` ancestor is a scroll container, and a
   position: sticky child resolves against its nearest one. Anything sticky
   inside .screen would have been pinned to a box that never scrolls, i.e.
   never pinned at all. Removing it is what lets the receipt button work. */
:root[data-layout="phone"] .screen {
  padding-bottom: calc(var(--screen-pad) + env(safe-area-inset-bottom, 0px));
}

/* THE DOCUMENT NEVER SCROLLS ON THE KIOSK.
   #app is already height-locked, but that only stops the document growing —
   it does not stop a screen's contents pushing past the frame and being lost
   behind the fixed bar. The screen is a bounded box; anything taller than it
   scrolls in a NAMED section inside (.pay-scroll, .choice-grid), never the
   page. Overscroll is contained so a swipe at the end of a list cannot start
   dragging the page itself — the rubber-band effect that makes an Android
   kiosk feel like a web page. */
:root[data-layout="kiosk"] .screen {
  overflow: hidden;
  overscroll-behavior: contain;
}
:root[data-layout="kiosk"] .choice-grid {
  overflow-y: auto;
  min-height: 0;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

/* --- Motion: entrance + press only, no loops --------------------------- */
.screen[data-enter] { animation: screenIn var(--dur-screen) var(--ease-out) both; }
@keyframes screenIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.stagger > * { animation: rise var(--dur-screen) var(--ease-out) both; }
.stagger > *:nth-child(1){animation-delay:.03s}.stagger > *:nth-child(2){animation-delay:.08s}
.stagger > *:nth-child(3){animation-delay:.13s}.stagger > *:nth-child(4){animation-delay:.18s}
.stagger > *:nth-child(5){animation-delay:.23s}.stagger > *:nth-child(6){animation-delay:.28s}
@keyframes rise { from { opacity: 0; transform: translateY(16px) scale(.98); } to { opacity: 1; transform: none; } }

/* --- Typography helpers ------------------------------------------------ */
/* Plus Jakarta Sans runs slightly wide; large sizes are tightened so headings
   hold together as a shape, and small caption text is opened up so it stays
   readable at a distance. Optical sizing, done by hand. */
.u-display { font-family: var(--font-display); font-size: var(--fs-display); font-weight: 800; letter-spacing: -0.025em; margin: 0; color: var(--ink); }
.u-heading { font-family: var(--font-display); font-size: var(--fs-h1); font-weight: 700; letter-spacing: -0.02em; margin: 0; color: var(--ink); }
.u-body    { font-size: var(--fs-body); margin: 0; letter-spacing: -0.003em; }
.u-caption { font-size: var(--fs-caption); color: var(--muted); margin: 0; font-weight: 500; letter-spacing: .005em; }

.credential {
  font-family: var(--font-credential);
  font-size: var(--fs-mono); font-weight: 600; letter-spacing: .06em;
  color: var(--ink); word-break: break-all; direction: ltr; unicode-bidi: isolate; text-align: start;
}
.u-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; }

/* --- Brand header ------------------------------------------------------ */
.appbar {
  flex: none; display: flex; align-items: center; gap: var(--sp-4);
  padding: var(--sp-3) var(--screen-pad);
  background: var(--bar-fill);
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--line);
  position: relative; z-index: 3;
}
.appbar::after { content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; background: var(--decor-gradient); opacity: .85; }
.appbar__logo { flex: none; border-radius: 50%; object-fit: contain; background: var(--paper); box-shadow: var(--logo-ring-sm); }
:root[data-layout="kiosk"] .appbar__logo { width: 52px; height: 52px; }
:root[data-layout="phone"] .appbar__logo { width: 38px; height: 38px; }
.appbar__name { font-family: var(--font-hero); font-weight: 700; color: var(--ink); font-size: var(--fs-h1); letter-spacing: -0.01em; }
:root[data-layout="phone"] .appbar__name { font-size: var(--fs-button); }
.appbar__spacer { flex: 1; }

/* --- Live clock — discreet, never competing with the shop's name ------- */
.clock { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 1px; line-height: 1.15; }
.clock__time {
  font-family: var(--font-hero); font-weight: 700; color: var(--ink);
  font-size: var(--fs-clock); letter-spacing: -0.01em; font-variant-numeric: tabular-nums;
}
.clock__date { font-size: var(--fs-date); color: var(--muted); font-weight: 500; }

/* =======================================================================
   WELCOME
   ======================================================================= */
.welcome { justify-content: center; align-items: center; text-align: center; gap: var(--sp-6); }

/* The brand block owns its own vertical rhythm — each element sets its own top
   margin so the logo → kicker → name → prompt cadence stays deliberate rather
   than an even gap between unequal things. */
.welcome__brand { display: flex; flex-direction: column; align-items: center; gap: 0; }

/* The language tiles arrive after the brand block has settled. */
.welcome__langs.stagger > *:nth-child(1) { animation-delay: .30s; }
.welcome__langs.stagger > *:nth-child(2) { animation-delay: .38s; }
.welcome .attribution { animation: rise var(--dur-screen) var(--ease-out) .48s both; }
.welcome__logowrap {
  position: relative; display: grid; place-items: center; padding: var(--sp-2);
}
/* A soft static glow. It used to pulse (haloPulse: opacity .7 -> 1 plus a
   scale) — but this element carries filter: blur(16px), so every frame of that
   animation forced the blur to be recomputed. That is one of the most
   expensive things a low-end tablet GPU can be asked to do, and the visible
   result was the logo area brightening and dimming on a loop.
   The blur is kept; it is now rasterised once and never again. */
.welcome__logowrap::before {
  content: ""; position: absolute; width: 168%; height: 168%; border-radius: 50%;
  background: var(--logo-halo);
  filter: blur(16px); z-index: -1;
  opacity: .85;
}
.welcome__logo {
  width: clamp(104px, 16vmin, 156px); height: clamp(104px, 16vmin, 156px);
  border-radius: 50%; object-fit: contain; background: var(--paper);
  box-shadow: var(--logo-ring-lg);
  animation: breathe var(--dur-breathe) var(--ease) infinite;
}
/* haloPulse deleted with the animation it drove — see .welcome__logowrap::before.
   breathe is transform-only (no opacity, no blur, no blend), so it composites
   on the GPU without repainting and stays on the phone. It is frozen on the
   kiosk with the rest of the ambient layer below. */
@keyframes breathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.035); } }

/* "Welcome to" — signage, not a caption: tracked small-caps in brand indigo,
   flanked by two hairlines that fade outwards. Prominent enough to be read
   across the counter, still clearly subordinate to the shop's name. */
.welcome__kicker {
  display: flex; align-items: center; justify-content: center;
  gap: clamp(12px, 2vmin, 22px);
  font-family: var(--font-hero); font-weight: 700; text-transform: uppercase;
  font-size: clamp(17px, 2.6vmin, 30px); letter-spacing: .34em;
  color: var(--kicker-ink); margin: var(--sp-6) 0 0;
}
/* the flanking rules */
.welcome__kicker::before, .welcome__kicker::after {
  content: ""; height: 2px; border-radius: var(--radius-full);
  width: clamp(30px, 7vmin, 96px);
}
.welcome__kicker::before { background: var(--rule-in); }
/* Letter-spacing leaves a trailing gap after the final letter, so the right
   rule sits further out than the left one. Pull it back by exactly that gap. */
.welcome__kicker::after  { background: var(--rule-out); margin-inline-start: -.34em; }
/* The shop's name is the hero and the only thing wearing the brand gradient. */
.welcome__title {
  font-family: var(--font-hero); font-weight: 800; letter-spacing: -0.03em; margin: var(--sp-1) 0 0;
  font-size: clamp(38px, 7.2vmin, 88px); line-height: 1.04;
  background: var(--title-gradient);
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; color: var(--title-fallback);
}
/* Optional, owner-editable, and absent by default. */
.welcome__tagline { color: var(--muted); font-weight: 500; font-size: var(--fs-body); margin: var(--sp-2) 0 0; }
.welcome__sub { color: var(--ink-soft); font-weight: 600; font-size: var(--fs-button); margin: var(--sp-3) 0 0; }


.welcome__langs { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-5); width: min(100%, 820px); margin-top: var(--sp-3); }
:root[data-layout="phone"] .welcome__langs { grid-template-columns: 1fr; gap: var(--sp-4); width: min(100%, 440px); }

.lang-tile {
  position: relative; overflow: hidden; display: flex; align-items: center; gap: var(--sp-5);
  min-height: var(--tap-min); padding: var(--sp-6) var(--sp-6);
  border: 2px solid transparent; border-radius: var(--radius-xl);
  background: var(--tile-solid) padding-box, var(--card-edge) border-box;
  box-shadow: var(--card-shadow); text-align: start;
  transition: transform .2s var(--ease-out), box-shadow .2s var(--ease-out);
}
:root[data-layout="kiosk"] .lang-tile { min-height: 136px; padding: var(--sp-6) var(--sp-7); gap: var(--sp-6); }
/* top-lit sheen + a soft brand glow that fades in on hover */
.lang-tile::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 46%; background: var(--tile-sheen); pointer-events: none; }
.lang-tile::after  { content: ""; position: absolute; inset: -1px; border-radius: inherit; box-shadow: var(--glow-brand); opacity: 0; transition: opacity .2s var(--ease-out); pointer-events: none; }
.lang-tile__badge {
  position: relative; flex: none; display: grid; place-items: center; width: 56px; height: 56px; border-radius: var(--radius-lg);
  background: linear-gradient(135deg, var(--info-tint), var(--primary-tint)); color: var(--primary);
  box-shadow: var(--tile-inner-edge); font-family: var(--font-hero); font-weight: 800; font-size: var(--fs-badge);
}
:root[data-layout="kiosk"] .lang-tile__badge { width: 72px; height: 72px; }
.lang-tile__name { position: relative; flex: 1; min-width: 0; font-family: var(--font-hero); font-weight: 700; font-size: var(--fs-button); color: var(--ink); white-space: nowrap; }
:root[data-layout="kiosk"] .lang-tile__name { font-size: var(--fs-card); }
.lang-tile__go { position: relative; flex: none; color: var(--muted); width: 28px; height: 28px; transition: transform .2s var(--ease-out), color .2s var(--ease-out); }
.lang-tile:hover { transform: translateY(-6px); box-shadow: var(--card-shadow-hover); }
.lang-tile:hover::after { opacity: .5; }
.lang-tile:active { transform: translateY(-1px) scale(.99); }
.lang-tile:hover .lang-tile__go { color: var(--primary); transform: translateX(4px); }

.attribution {
  margin-top: var(--sp-3); color: var(--muted); font-size: var(--fs-caption); font-weight: 500;
  background: none; border: 0; padding: var(--sp-2) var(--sp-3); border-radius: var(--radius-full);
}
.attribution strong { color: var(--ink-soft); font-weight: 700; }
.attribution__url { color: var(--info-ink); font-weight: 600; }
.attribution:hover { background: var(--hover-veil); }

/* =======================================================================
   HOME — enterprise service cards
   ======================================================================= */
.menu { gap: var(--stack-gap); }
.menu__grid { display: grid; gap: var(--sp-5); min-height: 0; }
/* Kiosk: a premium launcher, two columns by three rows — never a list. */
:root[data-layout="kiosk"] .menu__grid { flex: 1; grid-template-columns: repeat(2, 1fr); grid-auto-rows: 1fr; gap: var(--sp-6); }
:root[data-layout="kiosk"] .menu__grid[data-count="1"] { grid-template-columns: 1fr; }
:root[data-layout="phone"] .menu__grid { grid-template-columns: 1fr; gap: var(--sp-3); }

.card-svc {
  position: relative; overflow: hidden;
  display: flex; align-items: center; min-height: var(--tap-min); padding: var(--card-pad);
  border: 2px solid transparent; border-radius: var(--radius-xl);
  background: var(--tile-solid) padding-box, var(--card-edge) border-box;
  box-shadow: var(--card-shadow); text-align: start;
  transition: transform .2s var(--ease-out), box-shadow .2s var(--ease-out);
}
/* top-lit glass sheen */
.card-svc::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 46%; background: var(--tile-sheen); pointer-events: none; }
:root[data-layout="kiosk"] .card-svc { flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: var(--sp-5); padding: var(--sp-6); }
:root[data-layout="phone"] .card-svc { gap: var(--sp-4); }
.card-svc:hover { transform: translateY(-6px); box-shadow: var(--card-shadow-hover); }
.card-svc:active { transform: translateY(-1px) scale(.99); }

/* THE ICON CONTAINER — identical on every card, everywhere in the product.
   Same size, radius, fill, border, shadow and padding; only the glyph inside
   it changes. This single rule is what makes six services read as one designed
   system rather than six templates sharing a screen. */
.medallion {
  position: relative; flex: none; display: grid; place-items: center;
  border-radius: var(--radius-lg);
  background: var(--medallion-fill); color: var(--medallion-ink);
  box-shadow: var(--medallion-edge), var(--medallion-glow);
  transition: transform .2s var(--ease-out), box-shadow .2s var(--ease-out);
}
:root[data-layout="kiosk"] .medallion { width: 92px; height: 92px; border-radius: var(--radius-xl); }
:root[data-layout="phone"] .medallion { width: 52px; height: 52px; border-radius: var(--radius-md); }
/* Hairline strokes need a little more area to hold presence. */
.medallion svg { width: 52%; height: 52%; }

.card-svc__label { font-family: var(--font-hero); font-weight: 700; font-size: var(--fs-button); color: var(--ink); flex: 1; letter-spacing: -0.02em; }
:root[data-layout="kiosk"] .card-svc__label { flex: none; font-size: var(--fs-card); }
.card-svc__chev { flex: none; color: var(--muted); width: 22px; height: 22px; }
:root[data-layout="kiosk"] .card-svc__chev { display: none; }

/* WiFi — the signature service. It wears the full spectrum ring and medallion,
   which is what marks it out from the single-tone services. */


/* =======================================================================
   DETAIL SCREENS
   ======================================================================= */
.detail { gap: var(--stack-gap); }
.detail__head { display: flex; align-items: center; gap: var(--sp-4); }
.detail__head .medallion { width: 56px; height: 56px; }
:root[data-layout="kiosk"] .detail__head .medallion { width: 60px; height: 60px; }
.detail__body { flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-6); width: 100%; }
:root[data-layout="phone"] .detail__body { justify-content: flex-start; padding-top: var(--sp-4); }
.detail__side { display: flex; flex-direction: column; gap: var(--sp-4); width: min(100%, 560px); text-align: start; }

.panel { width: 100%; border-radius: var(--radius-lg); padding: var(--card-pad); display: flex; flex-direction: column; gap: var(--sp-4); background: var(--paper); border: 1px solid var(--line); box-shadow: var(--card-shadow); }
.field { display: flex; flex-direction: column; gap: 4px; }
.field__label { font-size: var(--fs-caption); color: var(--muted); font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }
.field__value { font-size: var(--fs-body); color: var(--ink); font-weight: 600; }

.qr-panel { position: relative; isolation: isolate; border-radius: var(--radius-xl); background: var(--paper); padding: var(--sp-4); display: grid; place-items: center; box-shadow: var(--card-shadow-hover); border: 1px solid var(--line); }
.qr-panel::before { content: ""; position: absolute; inset: -2px; border-radius: calc(var(--radius-xl) + 2px); background: var(--decor-gradient); z-index: -1; opacity: .55; }
.qr-panel__code { display: grid; place-items: center; }
.qr-panel__code canvas, .qr-panel__code img { display: block; width: 100%; height: auto; border-radius: 6px; }
:root[data-layout="kiosk"] .qr-panel__code { width: min(430px, 54vh); }
:root[data-layout="phone"] .qr-panel__code { width: min(260px, 62vw); }

/* --- QR column: the code, with its scan guide directly beneath --------- */
.qr-col { flex: none; display: flex; flex-direction: column; align-items: center; gap: var(--sp-4); }

/* The QR arrives with a short fade/scale, then a single halo pulse that
   expands OUTSIDE the panel — nothing ever moves over the modules themselves,
   so scan reliability is untouched. The pulse runs once and stops. */
/* The reveal animates SCALE ONLY — never opacity, and with no fill-mode.
   A stalled animation (backgrounded tab, paused compositor, animations
   disabled) holds its opening frame, and an opening frame of opacity 0 means
   an invisible QR: total failure of the one thing this screen exists for.
   Held at its opening frame this instead shows the code at 94% size, still
   perfectly scannable. The halo below carries the rest of the reveal. */
.qr-panel { animation: qrIn .42s var(--ease-out); }
@keyframes qrIn { from { transform: scale(.94); } to { transform: none; } }
.qr-panel::after {
  content: ""; position: absolute; inset: -5px; z-index: -1; pointer-events: none;
  border-radius: calc(var(--radius-xl) + 5px);
  box-shadow: 0 0 0 2px var(--info);
  opacity: 0;                       /* resting state: invisible, not a stuck ring */
  animation: qrHalo 1.3s var(--ease-out) .42s 1;
}
@keyframes qrHalo { 0% { opacity: .55; transform: scale(1); } 100% { opacity: 0; transform: scale(1.05); } }

/* Three numbered chips. Compact by design: this is a hint, not a manual. */
.qrguide {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: var(--sp-2);
  width: 100%; max-width: 430px;
}
.qrguide__item {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-4);
  border-radius: var(--radius-full);
  background: var(--surface-alt);
}
.qrguide__n {
  flex: none; display: grid; place-items: center; width: 26px; height: 26px;
  border-radius: 50%; background: var(--action-gradient); color: var(--primary-ink);
  font-family: var(--font-hero); font-weight: 800; font-size: var(--fs-eyebrow);
}
.qrguide__t { font-size: var(--fs-caption); color: var(--ink-soft); font-weight: 600; line-height: 1.25; }

/* --- Copyable payment details (public phone) --------------------------- */
.pay-hint {
  padding: var(--sp-3) var(--sp-4); border-radius: var(--radius-md);
  background: var(--info-tint); color: var(--info-ink); font-weight: 600; line-height: 1.35;
}
.copygrid { display: flex; flex-direction: column; gap: var(--sp-3); }
.copyfield {
  display: flex; flex-direction: column; gap: var(--sp-2);
  padding: var(--sp-4); border-radius: var(--radius-lg);
  border: 1px solid transparent;
  background: var(--tile-solid) padding-box, var(--card-edge) border-box;
  box-shadow: var(--card-shadow);
}
.copyfield .credential { font-size: var(--fs-credential); }
/* Copy buttons stay full-width and tall: they are the primary action on a
   phone, and a mis-tap means retyping an IBAN by hand. */
.copy-btn { min-height: var(--tap-min); }
.copy-btn.is-copied { background: var(--success-tint); color: var(--success-ink); border-color: var(--success); }
.copy-btn.is-copied .btn__icon { color: var(--success-ink); }

/* --- Help note — expectation, never a transaction state ---------------- */
.helpnote {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-4); border-radius: var(--radius-lg);
  background: var(--info-tint); border: 1px solid var(--tile-border);
}
.helpnote__icon { flex: none; width: 22px; height: 22px; color: var(--info-ink); margin-top: 2px; }
.helpnote__body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.helpnote__title { font-family: var(--font-hero); font-weight: 700; font-size: var(--fs-caption); color: var(--info-ink); }
.helpnote__text { font-size: var(--fs-caption); color: var(--ink-soft); line-height: 1.35; }

.chip { display: inline-flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-4); border-radius: var(--radius-full); font-size: var(--fs-caption); font-weight: 700; align-self: flex-start; max-width: 100%; }
.chip__icon { flex: none; width: 1.15em; height: 1.15em; }
.chip--fresh  { background: var(--primary-tint); color: var(--primary); }
.chip--cached { background: var(--surface-alt); color: var(--ink-soft); }
.chip--stale  { background: var(--warning-tint); color: var(--warning-ink); }

/* --- Payment layout: QR left, scrolling info column right -------------- */
.panel__title { font-family: var(--font-hero); font-weight: 700; font-size: var(--fs-button); color: var(--ink); margin: 0 0 var(--sp-1); }
.panel__next { margin-top: var(--sp-1); }
.panel--steps { gap: var(--sp-3); }
/* Info column: panels scroll, the Send Receipt action stays pinned. */
.detail__side--pay { gap: var(--sp-4); }
:root[data-layout="kiosk"] .detail__side--pay { height: 100%; }
:root[data-layout="kiosk"] .detail__side--pay .pay-scroll { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: var(--sp-4); padding-inline-end: var(--sp-1); }
:root[data-layout="phone"] .detail__side--pay .pay-scroll { display: flex; flex-direction: column; gap: var(--sp-4); }
.detail__side--pay > .btn { flex: none; }
/* Payment QR slightly smaller than the WiFi hero, leaving room for details. */
:root[data-layout="kiosk"] .detail__body--pay .panel--steps { padding: var(--sp-4); }
/* Stretch the payment row to full height so the info column has a bounded
   height (its panels scroll, the receipt button pins). The QR stays centred.
   Keyed on the orientation ATTRIBUTE, not a media query: the attribute is set
   by the same function that decides the layout mode, so CSS and JS can never
   disagree about which composition is on screen. */
:root[data-layout="kiosk"][data-orient="landscape"] .detail__body--pay { align-items: stretch; }
:root[data-layout="kiosk"][data-orient="landscape"] .detail__body--pay > .qr-col { align-self: center; }

/* --- Numbered steps — each step is its own small card, never a bare row --- */
.steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.steps__item {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--paper); border: 1px solid var(--line); border-radius: var(--radius-md);
  box-shadow: var(--card-shadow);
}
.steps__n { flex: none; width: 30px; height: 30px; border-radius: 50%; display: grid; place-items: center;
  background: var(--action-gradient); color: var(--primary-ink);
  font-family: var(--font-hero); font-weight: 800; font-size: var(--fs-eyebrow);
  box-shadow: var(--medallion-glow); }
.steps__t { flex: 1; font-size: var(--fs-step); color: var(--ink); line-height: 1.34; font-weight: 500; }
.pay-merchant { align-self: flex-start; font-weight: 700; color: var(--ink-soft); }

/* --- Credential fields — each becomes a card too (WiFi, account details) -- */
.panel--fields { background: none; border: 0; box-shadow: none; padding: 0; gap: var(--sp-3); }
.panel--fields .field {
  padding: var(--sp-4) var(--sp-5); gap: 6px;
  background: var(--tile-surface); border: 1px solid var(--tile-border);
  border-radius: var(--radius-lg); box-shadow: var(--card-shadow);
}

/* --- Tabs (Alfa Full / Installments) ----------------------------------- */
.tabs { display: flex; gap: var(--sp-2); padding: 4px; background: var(--surface-alt); border-radius: var(--radius-full); }
.tab { flex: 1; min-height: 48px; padding: var(--sp-2) var(--sp-4); border-radius: var(--radius-full);
  font-family: var(--font-hero); font-weight: 700; font-size: var(--fs-caption); color: var(--ink-soft);
  transition: background var(--dur-fast), color var(--dur-fast); }
.tab.is-active { background: var(--paper); color: var(--primary); box-shadow: var(--card-shadow); }

/* --- Choice cards (websites / social) ---------------------------------- */
.choice-grid { display: grid; gap: var(--sp-4); width: min(100%, 900px); margin-inline: auto; }
:root[data-layout="kiosk"] .choice-grid { grid-template-columns: repeat(2, 1fr); }
:root[data-layout="phone"] .choice-grid { grid-template-columns: 1fr; gap: var(--sp-3); }
.choice-card { position: relative; overflow: hidden; display: flex; align-items: center; gap: var(--sp-4); text-align: start; text-decoration: none; color: var(--ink);
  min-height: var(--tap-min); padding: var(--card-pad);
  border: 2px solid transparent; border-radius: var(--radius-xl);
  background: var(--tile-solid) padding-box, var(--card-edge) border-box;
  box-shadow: var(--card-shadow);
  transition: transform .2s var(--ease-out), box-shadow .2s var(--ease-out); }
.choice-card::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 46%; background: var(--tile-sheen); pointer-events: none; }
:root[data-layout="kiosk"] .choice-card { gap: var(--sp-5); padding: var(--sp-5) var(--sp-6); min-height: 128px; }
.choice-card > * { position: relative; }
.choice-card:hover { transform: translateY(-6px); box-shadow: var(--card-shadow-hover); }
.choice-card:active { transform: translateY(-1px) scale(.99); }
.choice-card__body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
/* A single long word (Facebook, Instagram) cannot wrap, so in a shrunk flex
   box it overflowed and the card's overflow:hidden sliced it — "Faceboo".
   Breaking is allowed as the last resort, and the name is sized to fit first. */
.choice-card__name { font-family: var(--font-hero); font-weight: 700; color: var(--ink);
  font-size: var(--fs-button); letter-spacing: -0.02em; line-height: 1.2;
  min-width: 0; overflow-wrap: anywhere; }
:root[data-layout="kiosk"] .choice-card__name { font-size: var(--fs-choice); }
:root[data-layout="kiosk"] .choice-card .medallion { width: 76px; height: 76px; border-radius: var(--radius-lg); }
.choice-card__cat { color: var(--muted); font-size: var(--fs-caption); line-height: 1.25; }
.choice-card--primary { grid-column: 1 / -1; }
.choice-card--primary .choice-card__name { color: var(--primary); }

.detail__body--top { justify-content: flex-start; padding-top: var(--sp-2); }

/* --- Scrollbars — slim, branded, no arrow buttons ---------------------- */
.pay-scroll { scrollbar-width: thin; scrollbar-color: var(--scroll-thumb) transparent; }
.pay-scroll::-webkit-scrollbar { width: 6px; }
.pay-scroll::-webkit-scrollbar-track { background: transparent; }
.pay-scroll::-webkit-scrollbar-thumb {
  background: var(--scroll-thumb); border-radius: var(--radius-full);
}
.pay-scroll::-webkit-scrollbar-thumb:hover { background: var(--scroll-thumb-hover); }
/* Chrome still reserves space for the stepper buttons unless they are removed. */
.pay-scroll::-webkit-scrollbar-button { display: none; height: 0; width: 0; }

/* --- Buttons ----------------------------------------------------------- */
.btn { position: relative; overflow: hidden; display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-3); min-height: var(--tap-min); padding: var(--sp-3) var(--sp-5); border-radius: var(--radius-md); font-family: var(--font-hero); font-weight: 700; font-size: var(--fs-button); transition: transform var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast), background var(--dur-fast); }
.btn--primary { background: var(--action-gradient); color: var(--primary-ink); box-shadow: var(--action-glow); }
.btn--primary:active { transform: translateY(1px) scale(.99); }
.btn--secondary { background: var(--paper); color: var(--primary); border: 1px solid var(--line); box-shadow: var(--card-shadow); }
.btn--secondary:active { transform: translateY(1px) scale(.99); border-color: var(--primary); }
.btn__icon { flex: none; width: 1.15em; height: 1.15em; }
.btn--wide { width: 100%; }

/* --- Chrome ------------------------------------------------------------ */
.chrome { flex: none; display: flex; gap: var(--sp-4); align-items: center; padding: var(--sp-4) var(--screen-pad); padding-bottom: calc(var(--sp-4) + env(safe-area-inset-bottom, 0px)); }
.chrome .btn { flex: 1; }

/* THE NAVIGATION BAR IS FURNITURE, NOT CONTENT.
   It used to be the last child of a flex column, which only LOOKED pinned
   because the kiosk frame was height-locked — the moment anything overflowed,
   or the layout fell to the phone branch, it slid away with the content. On an
   ATM the two buttons are always in the same place, and that is not something
   the content should be able to affect.

   Fixed to the viewport, above everything, with its own surface so content
   scrolling underneath never shows through. The screen reserves exactly the
   bar's height so nothing can hide behind it. */
:root[data-layout="kiosk"] .chrome {
  position: fixed;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: 30;
  background: var(--bar-fill);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  border-top: 1px solid var(--line);
  padding-block: var(--sp-4);
  padding-bottom: calc(var(--sp-4) + env(safe-area-inset-bottom, 0px));
}
/* The reserved strip. Measured from the same values the bar is built from, so
   the two cannot drift apart. */
:root[data-layout="kiosk"] {
  --chrome-h: calc(var(--tap-min) + (2 * var(--sp-4)) + env(safe-area-inset-bottom, 0px));
}
:root[data-layout="kiosk"] .screen { padding-bottom: calc(var(--chrome-h) + var(--sp-4)); }

/* --- Banners / countdown / empty / toast ------------------------------- */
.banner { flex: none; display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3) var(--screen-pad); font-size: var(--fs-caption); font-weight: 700; }
.banner--offline { background: var(--surface-alt); color: var(--ink-soft); }
.banner--warn { background: var(--warning-tint); color: var(--warning-ink); }
.banner__icon { flex: none; width: 1.2em; height: 1.2em; }
/* --- Auto-return indicator --------------------------------------------
   Replaces a dark card that slid over the screen for the final five seconds.
   That card arrived too late to answer the question it existed for — the
   first a customer knew of the timer was one already running out — and it
   covered the content they were still reading.

   This sits in the navigation bar for the whole thirty seconds instead, in
   the muted text colour at caption size. It is the lowest-weight element on
   the screen and never moves, so it reads as status rather than as an alarm.
   No colour change and no motion near the end: on a shop counter a pulsing
   red timer would rush a customer who is halfway through copying an IBAN. */
.autoreturn {
  flex: none;
  margin-inline-start: auto;
  padding-inline: var(--sp-3);
  font-size: var(--fs-caption);
  font-weight: 600;
  color: var(--muted);
  font-variant-numeric: tabular-nums;   /* the line must not jitter as it counts */
  white-space: nowrap;
  user-select: none;
}
/* One restrained step, and only in weight — the same text, slightly firmer. */
.autoreturn.is-soon { color: var(--ink-soft); font-weight: 700; }

/* --- Farewell — the brief branded return to Welcome -------------------- */
.farewell {
  position: fixed; inset: 0; z-index: 60;
  display: grid; place-items: center;
  background: var(--bg-field);
  opacity: 0; transition: opacity .3s var(--ease-out);
}
.farewell.is-in { opacity: 1; }
.farewell__card { display: flex; flex-direction: column; align-items: center; gap: var(--sp-5); }
.farewell__logo {
  width: clamp(96px, 15vmin, 140px); height: clamp(96px, 15vmin, 140px);
  border-radius: 50%; object-fit: contain; background: var(--paper);
  box-shadow: var(--logo-ring-lg);
  animation: farewellPop .5s var(--ease-out) both;
}
.farewell__text {
  margin: 0; font-family: var(--font-hero); font-weight: 700;
  font-size: clamp(22px, 3.4vmin, 38px); letter-spacing: -0.02em;
  background: var(--title-gradient);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: var(--title-fallback);
  animation: farewellPop .5s var(--ease-out) .08s both;
}
@keyframes farewellPop { from { opacity: 0; transform: translateY(12px) scale(.96); } to { opacity: 1; transform: none; } }
.empty { align-items: center; justify-content: center; text-align: center; gap: var(--sp-5); }
.empty__logo { width: clamp(84px, 15vmin, 132px); height: auto; border-radius: 50%; object-fit: contain; background: var(--paper); box-shadow: var(--logo-ring-md); }
.empty__mark { width: clamp(88px, 20vmin, 150px); aspect-ratio: 1; border-radius: var(--radius-full); background: var(--paper); border: 1px solid var(--line); box-shadow: var(--card-shadow); display: grid; place-items: center; color: var(--muted); }
.empty__mark svg { width: 44%; height: 44%; }
.toast { position: fixed; inset-block-end: calc(var(--sp-7) + env(safe-area-inset-bottom, 0px)); inset-inline-start: 50%; transform: translateX(-50%); background: var(--ink); color: var(--paper); padding: var(--sp-3) var(--sp-5); border-radius: var(--radius-full); font-size: var(--fs-caption); font-weight: 700; z-index: 20; box-shadow: var(--shadow-lg); animation: rise var(--dur-screen) var(--ease-out) both; }

/* --- Landscape kiosk: QR + details side by side ------------------------ */
:root[data-layout="kiosk"][data-orient="landscape"] .detail__body { flex-direction: row; align-items: center; justify-content: center; gap: var(--sp-8); width: min(100%, 1120px); margin-inline: auto; }
:root[data-layout="kiosk"][data-orient="landscape"] .detail__side { width: auto; flex: 1; min-width: 0; max-width: 520px; }
:root[data-layout="kiosk"][data-orient="landscape"] .qr-panel,
:root[data-layout="kiosk"][data-orient="landscape"] .qr-col { flex: none; }

@media (orientation: landscape) and (max-height: 700px) {
  :root[data-layout="kiosk"] .qr-panel__code { width: min(360px, 48vh); }
}

/* ======================================================================
   TABLET KIOSK — PORTRAIT
   ======================================================================
   The composition that did not exist. Portrait used to fall through to the
   phone branch and become a scrolling list; this gives it its own kiosk
   composition, stacked rather than side by side, but still a locked frame with
   tiles rather than rows.

   The rule throughout: the DOCUMENT never scrolls. Where content genuinely
   cannot fit, one named section inside the screen scrolls and the actions stay
   put — the same contract an ATM keeps. */

/* The QR stacks above its details, and gets the vertical room to stay the hero
   element rather than being squeezed by the panels beneath it. */
:root[data-layout="kiosk"][data-orient="portrait"] .detail__body {
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: var(--sp-6);
  width: min(100%, 760px);
  margin-inline: auto;
}
:root[data-layout="kiosk"][data-orient="portrait"] .detail__side {
  width: 100%;
  max-width: 680px;
  min-height: 0;
}
:root[data-layout="kiosk"][data-orient="portrait"] .qr-panel__code {
  width: min(400px, 34vh);
}

/* Payment in portrait: the QR keeps its size, the detail panels take the
   remaining height and scroll inside themselves, and Send Receipt never leaves
   the screen. */
:root[data-layout="kiosk"][data-orient="portrait"] .detail__body--pay { align-items: center; }
:root[data-layout="kiosk"][data-orient="portrait"] .detail__side--pay { height: auto; flex: 1; min-height: 0; }
/* Portrait shares one size across all three QR screens for the same reason
   landscape does: the frames must not differ between WiFi and a payment. */
:root[data-layout="kiosk"][data-orient="portrait"] .detail__body--pay .qr-panel__code,
:root[data-layout="kiosk"][data-orient="portrait"] .detail__body--wifi .qr-panel__code { width: min(340px, 30vh); }

/* The menu grid and the choice grid need no portrait rules: they are already
   two-up and equal-height under [data-layout="kiosk"], and a 10-inch tablet
   held upright is still ~800 CSS px wide — room for two tiles. They only ever
   collapsed to a single column because portrait was being served the phone
   layout. Fixing the mode fixed the grid. */
@media (max-width: 360px) { :root { --fs-display: 27px; --fs-h1: 22px; --fs-mono: 20px; } :root[data-layout="phone"] .medallion { width: 46px; height: 46px; } }

/* =======================================================================
   THE 10-INCH COUNTER TABLET, COMPOSED FOR ITS ACTUAL PANEL
   =======================================================================
   Reference hardware: Lenovo Tab A10 (A301LV), 1280x800, landscape, running
   fullscreen with no browser chrome. Everything below was measured on that
   geometry rather than estimated.

   THE PROBLEM THIS SOLVES. Before this block the two bars consumed 216px of
   800 — twenty-seven per cent of the panel spent on furniture. What remained
   was a 537px band that every screen had to live inside, so the home grid
   became two 592px letterbox strips, the pay button sat six pixels above the
   bottom bar, and the About QR filled four-fifths of the available height.

   Reclaiming 40px from the bars is what pays for all of it. Nothing here
   scales type or spacing globally; each screen is recomposed on its own.
   ======================================================================= */

/* --- The bars: 216px -> 176px ------------------------------------------
   The 96px tap target is a hard floor (CLAUDE.md section 7) and is NOT
   touched. What shrinks is the padding wrapped around it, which was sized
   for a phone, and the header's oversized name and clock. */
:root[data-layout="kiosk"] {
  --chrome-h: calc(var(--tap-min) + (2 * var(--sp-2)) + env(safe-area-inset-bottom, 0px));
}
:root[data-layout="kiosk"] .chrome {
  padding-block: var(--sp-2);
  padding-bottom: calc(var(--sp-2) + env(safe-area-inset-bottom, 0px));
}
:root[data-layout="kiosk"] .appbar {
  padding-block: var(--sp-2);
  gap: var(--sp-3);
}
:root[data-layout="kiosk"] .appbar__logo { width: 44px; height: 44px; }
:root[data-layout="kiosk"] .appbar__name  { font-size: var(--fs-shop); line-height: 1.2; }

/* The clock stacked time over date and stood 64px tall, which was the single
   thing setting the header's height — taller than the logo beside it. Laid
   out on one line it drops to about half that and the whole bar follows.
   Both values are kept: the date is still there, just no longer paying for
   its own row on a screen where vertical space is the scarce resource. */
:root[data-layout="kiosk"] .clock {
  flex-direction: row;
  align-items: baseline;
  gap: var(--sp-3);
  line-height: 1.25;
}
:root[data-layout="kiosk"] .clock > * { line-height: 1.25; }

/* Content now clears the bottom bar by a full step instead of five pixels.
   This is what stops the payment screens feeling crowded — the button has
   somewhere to sit. */
:root[data-layout="kiosk"] .screen {
  padding-top: var(--sp-5);
  padding-bottom: calc(var(--chrome-h) + var(--sp-5));
}

/* --- Home: three columns, and no orphan row ---------------------------
   Two columns produced 592px-wide cards barely 158px tall — a proportion
   that reads as a list pretending to be a grid. Three columns give roughly
   389x268, which is close to square and looks deliberate.

   Flex rather than grid, specifically so a partial last row CENTRES. The
   bound tablet shows six cards and fills 3x3 exactly; unbound it shows five,
   and grid would have left a visible hole in the corner. Flex composes those
   as three-over-two, centred, which looks chosen rather than broken. */
:root[data-layout="kiosk"] .menu__grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-content: center;
  gap: var(--sp-5);
}
/* height, NOT min-height. A menu label is shopkeeper-entered text of any
   length: "Is Kiosk Ke Baare Mein" already wraps to two lines, which grew its
   card from 238px to 279px and closed the gap to the navigation bar from 23px
   to 3px. min-height sets a floor with no ceiling, so a slightly longer label
   would have pushed the bottom row underneath the bar — CLAUDE.md rule 5 says
   nothing an admin types may break the kiosk. A fixed height bounds the grid
   at two rows whatever the text does, and the label clamps inside it. */
:root[data-layout="kiosk"] .menu__grid > * {
  flex: 0 1 calc((100% - (2 * var(--sp-5))) / 3);
  min-width: 300px;
  height: clamp(200px, 33vh, 264px);
}
/* Two lines is the most a card title may take. Beyond that the name is being
   used as a description, and the ellipsis is the honest outcome. */
:root[data-layout="kiosk"] .card-svc__label {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
/* A single card should not stretch across the whole screen. */
:root[data-layout="kiosk"] .menu__grid[data-count="1"] > * { flex: 0 1 420px; }

/* --- About: the QR was 436px in a 537px band -------------------------
   It is a link to open on a phone, not the subject of the screen. At this
   size it scans from well beyond arm's reach and leaves room for the
   heading and the three-step guide to breathe. */
:root[data-layout="kiosk"] .qr-panel__code { width: min(300px, 36vh); }

/* Payment QRs are the point of their screen and stay larger — but they are
   measured against the panel, not against a guess. */

/* --- Welcome: genuinely centred, by removing the cause of the offset ----
   The welcome screen measured 57px of space above the composition and 133px
   below. That was not a centring bug: .screen reserves --chrome-h of bottom
   padding so content clears the fixed navigation bar, and welcome is the one
   screen that HAS no navigation bar. It was holding a gap open for furniture
   that is not there.

   So the padding is released rather than compensated for with a margin. The
   flex centring that was always on the element then does the rest, and the
   block is optically centred because it is actually centred. */
:root[data-layout="kiosk"] .screen.welcome {
  padding-bottom: var(--sp-5);
}

/* --- Language tiles ---------------------------------------------------
   The count comes from LANGS in kiosk.js. Two options today, and the base
   two-column grid is correct for them, so there is deliberately no kiosk
   override of grid-template-columns here — an override fixed at two would
   have to be found and changed if a third language is ever switched back on.

   The third-tile stagger delay is kept for the same reason. It costs one
   line, it is inert while only two tiles exist, and its absence would be an
   invisible bug the day someone adds 'ur' back to LANGS. */
.welcome__langs.stagger > *:nth-child(3) { animation-delay: .46s; }

/* A long name degrades by wrapping rather than disappearing off the edge —
   white-space: nowrap used to clip "Roman Urdu" by 24px.

   break-word, NOT anywhere. `anywhere` may break INSIDE a word at any
   character, and on the real tablet it did exactly that: "English" rendered as
   "Engli / sh" and "Roman Urdu" as "Rom / an / Urdu". break-word only breaks a
   word that could not fit on a line of its own, which is the behaviour that
   was actually wanted. */
.lang-tile__name { min-width: 0; overflow-wrap: break-word; }
:root[data-layout="kiosk"] .lang-tile__name { white-space: normal; line-height: 1.2; }

/* The whole tile is the tap target, so the chevron earns nothing while costing
   28px of the name's width plus its gap. .card-svc__chev is already dropped on
   the kiosk for exactly this reason; this makes the two consistent. */
:root[data-layout="kiosk"] .lang-tile__go { display: none; }

/* With the chevron gone the name had 194px in landscape and "Roman Urdu"
   needs about 200 — it wrapped to two lines by four pixels. The inline
   padding was sized for a tile that still had a chevron to sit beside; sp-5
   returns those pixels and the name fits on one line. */
:root[data-layout="kiosk"] .lang-tile { padding: var(--sp-6) var(--sp-5); }

/* PORTRAIT: ONE TILE PER ROW.
   The counter tablet is meant to be locked landscape, but it is a tablet and
   it gets picked up. Two tiles across an 800px-wide panel left each name 92px
   of box — not enough for "English" at the kiosk type scale, which is what
   forced the wrap. Stacked, each tile spans the column and nothing wraps.
   Two options are a short list; there is no reason to squeeze them side by
   side on the narrow edge. */
:root[data-layout="kiosk"][data-orient="portrait"] .welcome__langs {
  grid-template-columns: minmax(0, 1fr);
  width: min(100%, 620px);
}

/* --- The 96px floor, applied where it was being missed -----------------
   CLAUDE.md section 7: every tap target on the kiosk is at least 96x96. Two
   controls predated that rule and were never brought up to it — the payment
   screen's Full/Installment tabs measured 48px, and the welcome attribution
   44px. Both are real targets a customer can hit with a thumb. */
:root[data-layout="kiosk"] .tab { min-height: var(--tap-min); }
:root[data-layout="kiosk"] .attribution {
  min-height: var(--tap-min);
  display: inline-flex; align-items: center; justify-content: center;
  padding-inline: var(--sp-5);
}

/* --- Urdu ---------------------------------------------------------------
   Nastaliq is a sloped, deeply-stacked script: it needs its own face and far
   more leading than Latin, or the descenders of one line collide with the
   line beneath. Both values already existed as tokens and simply had nothing
   applying them, because Urdu was never selectable.

   Keyed off <html lang>, which applyLanguage() already sets, so this needs no
   new state and covers every screen at once. The language tile is excluded:
   it must render its own name in its own script even before Urdu is chosen. */
:root[lang="ur"] body,
.lang-tile[data-lang="ur"] .lang-tile__badge,
.lang-tile[data-lang="ur"] .lang-tile__name {
  font-family: var(--font-urdu);
}
:root[lang="ur"] body { line-height: var(--lh-urdu); }

/* Nastaliq's tall stacking makes a Latin-tuned button feel cramped, and the
   badge glyph needs room to breathe rather than being scaled to fit. */
:root[lang="ur"] .btn,
:root[lang="ur"] .card-svc__label,
:root[lang="ur"] .choice-card__name { line-height: 1.7; }
:root[data-layout="kiosk"] .lang-tile[data-lang="ur"] .lang-tile__badge { font-size: calc(var(--fs-badge) - 4px); }

/* =======================================================================
   WEBSITES AND SOCIAL — composed for what this shop actually has
   =======================================================================
   Both screens used to inherit the generic two-column grid of 128px cards.
   With two websites that left roughly 450px of dead screen beneath them, and
   with three social platforms the full-width "open all links" card followed by
   a two-column grid produced 2 + 1: a single orphan card in the corner.

   Neither screen is a list. Each is a small, fixed set of choices that should
   fill the band it has been given, so the composition is driven by data-count
   rather than by a fixed template.

   Both keep the existing behaviour where a card on the kiosk NEVER navigates —
   it opens a QR sub-screen. That is what keeps the counter tablet from
   wandering off to Facebook with no way back, and it is what makes the screen
   compatible with the Fully Kiosk URL whitelist. Do not turn these into links.
   ======================================================================= */

/* The body is a flex column; let the grid claim the whole band so the cards
   have a height to fill rather than collapsing to their content. */
:root[data-layout="kiosk"] .detail__body--top {
  flex: 1; min-height: 0; display: flex; flex-direction: column;
}
:root[data-layout="kiosk"] .detail__body--top > .choice-grid { flex: 1; }

/* --- Websites ---------------------------------------------------------
   Two entries, so two tall panels side by side. The row layout that suits a
   phone list wastes a tall card, so these switch to a column: a large
   medallion, the site name, and its descriptor centred beneath. */
:root[data-layout="kiosk"] .choice-grid--sites {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-auto-rows: 1fr;
  width: min(100%, 960px);
  align-content: stretch;
}
:root[data-layout="kiosk"] .choice-grid--sites[data-count="1"] {
  grid-template-columns: minmax(0, 520px);
  justify-content: center;
}
/* Four or more and the tall panel stops making sense — go back to a compact
   row so nothing has to stretch absurdly. */
:root[data-layout="kiosk"] .choice-grid--sites[data-count="4"],
:root[data-layout="kiosk"] .choice-grid--sites[data-count="5"],
:root[data-layout="kiosk"] .choice-grid--sites[data-count="6"] {
  grid-auto-rows: minmax(128px, auto);
  align-content: center;
}
/* Sized against the viewport rather than stretched to fill. The landscape
   .detail__body rule centres its children, and these elements carry BOTH
   detail__body and detail__body--top, so that rule wins on specificity and a
   stretched grid would be fighting it. Giving the cards a real height and
   letting the centring do its job is the simpler half of that argument. */
:root[data-layout="kiosk"] .choice-grid--sites .choice-card {
  flex-direction: column; justify-content: center; text-align: center;
  gap: var(--sp-4); padding: var(--sp-6);
  min-height: clamp(240px, 42vh, 340px);
}
:root[data-layout="kiosk"] .choice-grid--sites .choice-card__body {
  flex: none; align-items: center; gap: var(--sp-2);
}
:root[data-layout="kiosk"] .choice-grid--sites .medallion { width: 112px; height: 112px; }
:root[data-layout="kiosk"] .choice-grid--sites .choice-card__cat { font-size: var(--fs-body); }
/* The whole panel is the target; a chevron on a centred column is noise. */
:root[data-layout="kiosk"] .choice-grid--sites .card-svc__chev { display: none; }

/* --- Social -----------------------------------------------------------
   "Open all links" is the primary action and keeps its full-width band at the
   top. The platforms then take one row underneath, one column each, which is
   what removes the orphan: three platforms make three columns, not 2 + 1. */
:root[data-layout="kiosk"] .choice-grid--social {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-template-rows: auto 1fr;
  width: min(100%, 960px);
}
:root[data-layout="kiosk"] .choice-grid--social[data-count="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
:root[data-layout="kiosk"] .choice-grid--social[data-count="4"] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
/* Five or more wrap onto a second row rather than becoming slivers. */
:root[data-layout="kiosk"] .choice-grid--social[data-count="5"],
:root[data-layout="kiosk"] .choice-grid--social[data-count="6"] {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-template-rows: auto 1fr 1fr;
}
:root[data-layout="kiosk"] .choice-grid--social .choice-card--primary {
  grid-column: 1 / -1;
  min-height: var(--tap-min);
}
:root[data-layout="kiosk"] .choice-grid--social .choice-card:not(.choice-card--primary) {
  flex-direction: column; justify-content: center; text-align: center;
  gap: var(--sp-4); padding: var(--sp-5);
  min-height: clamp(180px, 32vh, 250px);
}
:root[data-layout="kiosk"] .choice-grid--social .choice-card:not(.choice-card--primary) .choice-card__body {
  flex: none; align-items: center;
}
:root[data-layout="kiosk"] .choice-grid--social .choice-card:not(.choice-card--primary) .medallion {
  width: 96px; height: 96px;
}
:root[data-layout="kiosk"] .choice-grid--social .card-svc__chev { display: none; }

/* =======================================================================
   PRODUCTION POLISH — spacing and hierarchy only
   No new colours, no new type, no new components. Every value below moves
   something that already existed.
   ======================================================================= */

/* A REPLACEMENT QR IMAGE MUST NOT REQUIRE A LAYOUT CHANGE.
   The Alfa QR is a placeholder and will be swapped for the real merchant code,
   which will not be the same pixel dimensions. aspect-ratio holds the frame
   square whatever arrives, and object-fit: contain fits the code inside it
   without cropping or stretching — a stretched QR does not scan. */
/* The scaled image must not spill past its frame. fitQrImages() enlarges an
   uploaded QR until its dark modules fill the box, and the white margin baked
   into the file is what gets clipped away. That is safe: .qr-panel is white
   and pads the code by a full step, so the frame itself supplies the quiet
   zone a scanner needs. */
.qr-panel__code { overflow: hidden; border-radius: 6px; }
.qr-panel__code img { aspect-ratio: 1 / 1; object-fit: contain; }

/* --- Payments ---------------------------------------------------------
   The three scan-instruction cards are gone (see qrGuide in kiosk.js), which
   returns roughly 150px to this screen. It goes to the QR, which is the thing
   the customer is actually here to point a phone at. */

/* With the guide gone the QR column is shorter than the details column, so
   the two are aligned at the top rather than about their centres. That pulls
   the account number up to where the eye already is instead of leaving it
   floating in the middle of the screen. */
:root[data-layout="kiosk"][data-orient="landscape"] .detail__body--pay { align-items: flex-start; }
:root[data-layout="kiosk"][data-orient="landscape"] .detail__body--pay > .qr-col { align-self: flex-start; }

/* --- ONE QR FRAME, THREE SCREENS --------------------------------------
   WiFi, Bank Transfer and Alfa Pay now present an identical frame: the same
   .qr-panel border, radius, shadow and padding, wrapping the same code width.
   The payment screens used to carry a --qr-panel--image modifier that cut the
   padding from 16px to 12px, on the theory that an uploaded QR usually has its
   own white quiet zone built in while a generated one does not. That is true
   of the artwork and irrelevant to the customer, who sees two frames of
   different weight for the same kind of object. The modifier is deleted, not
   overridden, so nothing can drift apart again.

   One rule, three screens — change this and all three move together. */
:root[data-layout="kiosk"] .detail__body--wifi .qr-panel__code,
:root[data-layout="kiosk"] .detail__body--pay  .qr-panel__code { width: min(380px, 48vh); }
:root[data-layout="kiosk"] .detail__body--wifi .detail__side {
  gap: var(--sp-3);
  width: min(100%, 470px);
}
:root[data-layout="kiosk"][data-orient="landscape"] .detail__body--wifi { align-items: flex-start; }
:root[data-layout="kiosk"][data-orient="landscape"] .detail__body--wifi > .qr-col { align-self: flex-start; }
/* Portrait stacks, so the gap between QR and credentials is vertical and a
   full step is too much once the QR is larger. */
:root[data-layout="kiosk"][data-orient="portrait"] .detail__body--wifi { gap: var(--sp-5); }

/* --- PUBLIC PHONE: NAVIGATION STAYS WITHIN REACH ----------------------
   On the phone link the page scrolls, and Back/Home sat at the very bottom of
   the document. On a long payment screen a customer had to scroll past all the
   bank details to reach them, which is the wrong way round: the way out should
   never be further away than the content.

   sticky, deliberately NOT fixed. A fixed bar is removed from flow and would
   sit permanently on top of the last line of content — the exact problem the
   kiosk layout has to solve with a reserved --chrome-h of padding. A sticky bar
   keeps its place in the document, so it reserves its own space at the end and
   nothing can be hidden behind it, while still riding the bottom of the
   viewport during the scroll.

   It needs its own background because the base .chrome has none: while stuck,
   content scrolls underneath it. */
:root[data-layout="phone"] .chrome {
  position: sticky;
  inset-block-end: 0;
  z-index: 30;
  background: var(--bar-fill);
  -webkit-backdrop-filter: blur(10px);
          backdrop-filter: blur(10px);
  border-top: 1px solid var(--line);
}

/* =======================================================================
   PUBLIC PHONE: SEND RECEIPT STAYS WITHIN REACH
   =======================================================================
   Stack from the bottom of the viewport:

       1. Back / Home navigation      sticky, bottom 0
       2. Send Receipt on WhatsApp    sticky, bottom --nav-h
       3. payment content             scrolls behind both

   On the kiosk this button is already pinned, by .detail__side--pay being
   full height with .pay-scroll taking the slack. The phone has no such
   container — the page scrolls — so the button sat at the very end of a
   1580px column. A customer had to scroll past every account field to reach
   the action those fields exist for.

   The EXISTING button is made sticky. Nothing is duplicated into the flow:
   it is still the same single <a> that screenPayment() renders, still the
   same gradient, icon, height and href.

   --nav-h is the navigation bar's height expressed once, and the bar is
   pinned to it with min-height so the two cannot disagree. Both include the
   safe-area inset, so on an iPhone the whole stack lifts above the home
   indicator together. Measured: nav button 52.59px + 2x16px padding, so 54px
   is the smallest whole number that cannot overlap. */
:root[data-layout="phone"] {
  --nav-h: calc(54px + (2 * var(--sp-4)) + env(safe-area-inset-bottom, 0px));
}
:root[data-layout="phone"] .chrome { min-height: var(--nav-h); }

:root[data-layout="phone"] .detail__side--pay > .btn {
  position: sticky;
  inset-block-end: var(--nav-h);
  /* Below .chrome's 30, so if a sub-pixel rounding ever brought them into
     contact the navigation would paint over the button, never the reverse. */
  z-index: 20;
}

/* Sticky keeps the button in the document, so it reserves its own space and
   the last account field can never end up underneath it. This is the gap
   BETWEEN the two, so the content does not stop flush against the button. */
:root[data-layout="phone"] .detail__side--pay .pay-scroll { padding-bottom: var(--sp-2); }

/* --- Reduced motion — premium with everything off --------------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; scroll-behavior: auto !important; }
}
