/* Nocturne bones, the KIPPO skin — the arcade re-theme of 31-8-2026.
 *
 * The structure is still the 16-8-2026 Nocturne handoff — hairline rules,
 * text tabs, plain numbers instead of stat boxes, status carried by a word
 * and a colour. The COLOUR, TYPE AND GEOMETRY are Kippo (theme.css +
 * DESIGN.md): a pure black void, everything drawn as a 1px white outline,
 * monospace at every size, and ONE hot magenta (#ee1f66) rationed so it
 * reads as a power-up rather than a brand wash. Cards are transparent panels
 * — the border is the card. No shadows: depth is overlap and outline
 * contrast. Buttons 10px, cards 15px, nothing rectangular past 15px.
 *
 * Five departures from the Kippo reference, all deliberate and all forced by
 * what this product is:
 *
 *   THE FONT IS A PAIR, NOT A SINGLETON. Kippo says one typeface and no
 *   fallback sans. Source Code Pro has no Arabic — not one glyph — and this
 *   product is Arabic-first, so a monospace-only rule would render the entire
 *   panel in a browser default. The stack is "Source Code Pro", then Tajawal:
 *   the browser resolves it per glyph, so every Latin letter, every digit and
 *   every symbol is Kippo's monospace and the Arabic is Tajawal. Both are
 *   SELF-HOSTED from the repo's own committed files, never Google Fonts —
 *   the panel loads nothing from anywhere and the service worker has to serve
 *   the whole app offline. Source Code Pro is subset to Latin (17KB a face,
 *   lighter than the Tajawal beside it) with the 'zero' feature kept, because
 *   the slashed zero is the terminal identity and this app is mostly numbers.
 *
 *   TRACKING IS A LATIN DEVICE. Kippo asks for 0.1em on all UI text and
 *   0.3–0.5em on display. Arabic letters JOIN; spacing them apart pulls a
 *   word into disconnected stumps. So the caps-and-tracking treatment is
 *   applied through .kcaps / [lang] on Latin runs and on the numerals, and
 *   Arabic keeps its natural fit. text-transform: uppercase is a no-op on
 *   Arabic, so it stays global and costs nothing.
 *
 *   STATUS KEEPS FOUR VOICES. Kippo forbids red/green/amber semantics and
 *   says the whole vocabulary is black + white + one pink. A counter has to
 *   be read at two metres while someone is talking to you, and CLAUDE.md
 *   section 6 requires status to be a word AND a colour. Translated into
 *   Kippo's own palette rather than out of it: free is the plain white
 *   outline, in use is the pink power-up, promised is Terminal Cyan (#33beff,
 *   already a token here), maintenance is Ash and dimmed. No green, no amber,
 *   no traffic lights — four outline weights in the system's own colours.
 *
 *   THE ROOM-TYPE BADGE IS A GHOST OUTLINE, carrying the owner's own words
 *   and no colour of its own. It used to hash a hue out of the type text, so
 *   that any type the owner invented got a stable one; five chromatic
 *   outlines on one grid is precisely the brand wash the don't-list forbids,
 *   and the free-text rule (CLAUDE.md section 6) only ever asked for the
 *   words to be printed and grouped, never branched on. --ty is still
 *   honoured if a café ever asks for the colour back.
 *
 *   The shell keeps `overflow-y: auto` on the scrolling region where the
 *   design clips. The design assumes eight rooms; a café with twenty must not
 *   have its last row clipped away with no way to reach it.
 *
 *   Nothing is sized in pixels except the borders and the radii, which are
 *   the two things Kippo states in pixels and means in pixels. Everything
 *   else is a rem against a root that grows with the viewport, so the whole
 *   system scales together instead of the counter reading a 14px label from
 *   an arm's length away. Density is then a handful of token overrides.
 *
 * Arabic first, so everything uses logical properties and survives dir="rtl".
 */

:root {
  /* The browser's own parts — a time field's clock, a date's calendar, the
     native pickers, form scrollbars — drawn for a dark page. Without it they
     came in the OS's light scheme: the reservation's clock icon was black on
     the void, invisible, and the picker opened as a white flash at night. */
  color-scheme: dark;

  /* Kippo's surfaces: the void, the barely-there carbon panel, ash for a
     divider where white would shout. */
  --color-bg: #000000;
  --color-surface: #000000;
  --color-elevated: #29292a;
  --color-carbon: #29292a;
  --color-ash: #333333;
  --color-text: #ffffff;
  --color-accent: #ee1f66;
  --color-divider: #333333;

  --color-neutral-100: #ffffff;
  --color-neutral-200: rgba(255, 255, 255, 0.92);
  --color-neutral-300: rgba(255, 255, 255, 0.82);
  --color-neutral-400: rgba(255, 255, 255, 0.70);
  --color-neutral-500: rgba(255, 255, 255, 0.58);
  --color-neutral-600: rgba(255, 255, 255, 0.46);
  --color-neutral-700: rgba(255, 255, 255, 0.34);
  --color-neutral-800: #333333;
  --color-neutral-900: #29292a;

  --color-accent-100: #fde7ef;
  --color-accent-200: #fbc2d6;
  /* 300 is the accent itself, not a tint. Everywhere this file says "accent
     text" it must be the one true #ee1f66 — Kippo's don't-list is explicit
     that diluting the pink is how the system dies. 600 and 700 exist only as
     the hover and press states of the filled button. */
  --color-accent-300: #ee1f66;
  --color-accent-400: #f34a80;
  --color-accent-500: #ee1f66;
  --color-accent-600: #d01755;
  --color-accent-700: #a81145;
  --color-accent-800: #6e0c2e;
  --color-accent-900: #3a0619;

  /* The two decorative gradients, and nothing else uses colour. */
  /* Kippo names two decorative gradients. The warm one starts in gold, so
     with gold out of this product it uses the cool one — the reference's own
     alternative rather than an invented blend. */
  --grad-terminal: linear-gradient(to right, #33beff, #33ffb8);

  /* Status, in the system's own voices: white / pink / cyan / ash.
     --ok is the plain outline, so "free" is literally the unmarked card. */
  --ok: #ffffff;
  --ok-deep: #ffffff;
  --ok-bg: transparent;
  --busy: #ee1f66;
  --busy-bg: transparent;
  --resv: #33beff;
  --resv-bg: transparent;
  /* The one green in the product: the connection dot in the header, and
     nothing else. "Online" is the single signal a person checks without
     reading it, and green is what everybody already means by it. It sits
     deliberately outside the status vocabulary above — rooms never use it —
     so it stays a dot in the chrome rather than a fifth voice on the grid. */
  --color-online: #2ee6a0;

  /* A second colour lived here for a day — Sunset Gold, carrying every figure
     that was not money — and the client asked for it out on 31-8-2026. What
     it was solving was real: the magenta had quietly taken four jobs, and a
     colour that means everything means nothing. The answer without a second
     hue is that the accent keeps ONE job and the rest is said by weight:

         MAGENTA ACTS.  WEIGHT NOTES.

     Magenta is anything a person is meant to do something about — the CTA,
     the chosen chip, the tab you are on — plus the one session running right
     now, which is the live thing rather than a number to file away. Money is
     plain white on the void, the loudest pairing this palette has, because it
     is what the owner opens the app for. Every other figure is quiet grey,
     and the standout of a set is bold rather than coloured.

     `--gold` survives only as the stars' alias so the rating rules below did
     not all need rewriting; it resolves to the accent. */
  --gold: var(--color-accent);

  /* Kippo's radii, exactly: 10 on buttons and tags, 15 on cards and images,
     50 on icon buttons, and never past 15 on a rectangle. */
  --radius-sm: 10px;
  --radius-md: 10px;
  --radius-lg: 15px;
  --radius-round: 50px;

  /* The hairline that IS the card. One value, so a border is never invented
     locally at some other weight. */
  --hair: 1px solid var(--color-text);
  --hair-quiet: 1px solid var(--color-ash);

  /* Body leading. Kippo asks for 1.88, which is a marketing page's air; a
     counter sheet with nine fields in it cannot afford that and stay on one
     screen, so the app runs at 1.7 and dense mode takes it to 1.55 with the
     rest of the scale. A token, because it is a density decision like every
     other one here — not a number to retype in forty rules. */
  --lh-body: 1.7;
  /* And the leading for text that is a LINE rather than a paragraph — a
     field label, a heading, a row in the bill. Kippo's generous leading is a
     body-copy value; spending it on forty single-line labels is how a sheet
     grows a scrollbar it does not need. */
  --lh-ui: 1.35;

  /* UI tracking. Latin only — see the header; Arabic never receives it. */
  --track-ui: 0.1em;
  --track-display: 0.28em;

  /* The type ladder. --fs-2xl is body text; everything else steps off it.
     Named as a ladder rather than by role because the same step is a room name
     in one place and a table cell in another. */
  --fs-3xs: 0.71rem;
  --fs-2xs: 0.75rem;
  --fs-xs: 0.79rem;
  --fs-sm: 0.82rem;
  --fs-md: 0.86rem;
  --fs-base: 0.89rem;
  --fs-lg: 0.93rem;
  --fs-xl: 0.96rem;
  --fs-2xl: 1rem;
  --fs-3xl: 1.07rem;
  --fs-4xl: 1.14rem;
  --fs-5xl: 1.36rem;
  --fs-6xl: 1.43rem;
  --fs-7xl: 1.57rem;
  --fs-8xl: 1.86rem;
  --fs-9xl: 2.29rem;
  --fs-10xl: 2.71rem;
  /* The dashboard hero alone — 56px on the artboard. */
  --fs-11xl: 3.3rem;

  /* Spacing, on the same footing: dense mode retunes these four and the whole
     app tightens without a single rule being restated. */
  --sp-1: 0.21rem;
  --sp-2: 0.36rem;
  --sp-3: 0.57rem;
  --sp-4: 0.86rem;
  --sp-5: 1.14rem;
  --sp-6: 1.43rem;
  --sp-7: 2rem;
  --sp-8: 3.14rem;

  /* The gutter. Wider on a counter screen, thumb-width on a phone. */
  --pad-x: clamp(1rem, 2.2vw, 2rem);

  --bar-h: 3.86rem;
  --room-h: 9.9rem;
  --chart-h: 8.8rem;
  --sheet-px: 2.1rem;

  /* Nocturne's rule faded at both ends. Kippo's rules do not fade — a border
     is a border, drawn at full weight in ash. One token, so every freestanding
     line in the app is this and nothing else. */
  --rule: var(--color-ash);
}

/* Source Code Pro, subset to Latin, from the repo's own files. It carries
   every letter, digit and symbol; Arabic falls through to Tajawal on the next
   line of the stack, glyph by glyph, which is the only way a monospace-only
   design and an Arabic-first product can both be true. */
@font-face {
  font-family: "Source Code Pro";
  src: url("fonts/SourceCodePro-Regular.ttf") format("truetype");
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: "Source Code Pro";
  src: url("fonts/SourceCodePro-Semibold.ttf") format("truetype");
  font-weight: 500 600;
  font-display: swap;
}
@font-face {
  font-family: "Source Code Pro";
  src: url("fonts/SourceCodePro-Bold.ttf") format("truetype");
  font-weight: 700 900;
  font-display: swap;
}

/* The same three files again, minus ONE character: the space.
 *
 * The browser takes each glyph from the first family that has it, and Source
 * Code Pro has a space — a monospace one, 0.6em, twice Tajawal's. So in the
 * Arabic shell every gap between two Arabic words was a monospace gap, and
 * "غرفة   رقم   3" read as three stumps laid out on a grid: the single loudest
 * sign that the pair was assembled rather than set. This family is what the
 * RTL shell leads with; its unicode-range leaves U+0020 out, so the space
 * falls through to Tajawal's and Arabic sets at its natural word spacing,
 * while every Latin letter and digit is still the monospace. The English
 * shell keeps the full face — its monospace gaps ARE the terminal look.
 * The no-break space (U+00A0) goes with it: money() and hhmm() join "30" and
 * "ج" with one so an amount never breaks, and left in this face it set every
 * amount's gap at the monospace width — "30   ج" — where the plain space it
 * replaced was Tajawal's. */
@font-face {
  font-family: "Source Code Pro RTL";
  src: url("fonts/SourceCodePro-Regular.ttf") format("truetype");
  font-weight: 400;
  font-display: swap;
  unicode-range: U+0-1F, U+21-9F, U+A1-10FFFF;
}
@font-face {
  font-family: "Source Code Pro RTL";
  src: url("fonts/SourceCodePro-Semibold.ttf") format("truetype");
  font-weight: 500 600;
  font-display: swap;
  unicode-range: U+0-1F, U+21-9F, U+A1-10FFFF;
}
@font-face {
  font-family: "Source Code Pro RTL";
  src: url("fonts/SourceCodePro-Bold.ttf") format("truetype");
  font-weight: 700 900;
  font-display: swap;
  unicode-range: U+0-1F, U+21-9F, U+A1-10FFFF;
}

/* Tajawal, from the repo's own files — the Arabic half of the pair, without a
   single request leaving the machine. */
@font-face {
  font-family: "Tajawal";
  src: url("fonts/Tajawal-Regular.ttf") format("truetype");
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: "Tajawal";
  src: url("fonts/Tajawal-Bold.ttf") format("truetype");
  font-weight: 500 700;
  font-display: swap;
}
@font-face {
  font-family: "Tajawal";
  src: url("fonts/Tajawal-ExtraBold.ttf") format("truetype");
  font-weight: 800 900;
  font-display: swap;
}

* { box-sizing: border-box; }

/**
 * The app is a fixed shell, not a document — the page never scrolls, only the
 * content region does. dvh rather than vh so a phone's address bar cannot make
 * the shell taller than the phone.
 *
 * The root size is the whole responsive story: it interpolates from 15px at a
 * 390px phone to 18px at a 1920px counter monitor, and everything below is a
 * rem against it.
 */
html {
  height: 100%;
  /* One step up from the original 15–18px ramp — the modernisation pass
     (22-8-2026): the counter reads this from an arm's length. */
  font-size: clamp(16px, 0.196vw + 15.24px, 19px);
}

body {
  margin: 0;
  height: 100dvh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  background: var(--color-bg);
  color: var(--color-text);
  /* The pair. Source Code Pro first so it claims every Latin glyph and every
     digit; Tajawal picks up the Arabic behind it. */
  font-family: "Source Code Pro", "Tajawal", ui-monospace, SFMono-Regular,
    Menlo, Monaco, Consolas, "Noto Naskh Arabic", monospace;
  font-size: var(--fs-2xl);
  line-height: var(--lh-body);
  /* The slashed zero — Kippo's terminal signature, and this app is mostly
     numbers, so it earns its keep beyond the reference. */
  font-feature-settings: "zero";
  -webkit-text-size-adjust: 100%;
}
/* The Arabic shell leads with the space-less copy of the monospace (see its
   @font-face), so Arabic words sit at Tajawal's own spacing. */
html[lang="ar"] body {
  font-family: "Source Code Pro RTL", "Tajawal", ui-monospace, SFMono-Regular,
    Menlo, Monaco, Consolas, "Noto Naskh Arabic", monospace;
}

/* Times, money and counts line up in columns. Applied by class rather than
   globally so Arabic body text is never touched. */
.num { font-variant-numeric: tabular-nums; }

/* One line, not a paragraph: headings, field labels and the rows of a bill
   or a breakdown are each a single line, and they take the tighter leading. */
.head h2, .sub-head, .sheet h2, .sheet h3, label,
.line-row, .total-row, .row-title, .breakdown .brow, .tbl-row, .stats .stat,
.figure, .empty, .hint, .helper { line-height: var(--lh-ui); }

/* Kippo's uppercase-plus-tracking treatment, as one utility.
 *
 * uppercase is safe everywhere — Arabic has no case, so the declaration is a
 * no-op on it. TRACKING IS NOT SAFE: Arabic letters join, and letter-spacing
 * pulls a word into disconnected stumps. So the spacing is applied only where
 * the run is Latin — an explicit lang, the .ltr isolate the app already uses
 * for numbers, or the English locale on the root. Arabic gets the caps
 * treatment's weight and size and keeps its own fit. */
.kcaps { text-transform: uppercase; font-weight: 700; }
.kcaps:lang(en), html[lang="en"] .kcaps,
.kcaps .ltr, .ltr.kcaps, .num.kcaps { letter-spacing: var(--track-ui); }

#app {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column;
  width: 100%; max-width: 1680px; margin: 0 auto;
}

button { font: inherit; cursor: pointer; }

/* One focus ring for the whole app. Keyboard only — :focus-visible, so a mouse
   press on a room card does not leave a ring behind it. */
:focus-visible {
  outline: 2px solid var(--color-accent-400);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
/* A sheet takes the focus itself when it opens with no field to put the caret
   in (app.js), so a screen reader hears which dialog opened. It is already
   drawn as its own white outline; a ring around the whole dialog would say
   nothing the outline does not. */
.sheet:focus-visible { outline: none; }

/* Words for a screen reader alone: the live regions in index.html and the
   file input behind the menu sheet's own button. Still in the page, so still
   read and still focusable — only not drawn. */
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* The content region scrolls; its scrollbar should belong to the theme
   rather than to the operating system's light one. */
main, .sheet {
  scrollbar-width: thin;
  scrollbar-color: var(--color-neutral-800) transparent;
}
main::-webkit-scrollbar, .sheet::-webkit-scrollbar { width: 8px; }
main::-webkit-scrollbar-thumb, .sheet::-webkit-scrollbar-thumb {
  background: var(--color-neutral-800); border-radius: 4px;
}

/* The fading rule, as an element. */
.hr { height: 1px; background: var(--rule); flex: 0 0 auto; }

/* ---------------------------------------------------------------- buttons -- */

/* Kippo has exactly two: a filled magenta primary — the ONLY solid fill in
   the system — and a ghost, transparent behind a 1px white outline. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  font: inherit; font-size: var(--fs-md); font-weight: 700; line-height: 1.2;
  text-transform: uppercase;
  color: var(--color-text); background: transparent;
  border: 1px solid transparent; border-radius: var(--radius-md);
  padding: var(--sp-3) var(--sp-4); white-space: nowrap;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
/* Touch targets everywhere, not only under pointer:coarse — several counter
   machines have touchscreens the media query cannot see. */
.btn { min-height: 2.6rem; }
/* The one filled button in the whole product. Hover and press go DARKER,
   never lighter. White on the resting #ee1f66 is 4.18:1 — a known gap in the
   Kippo reference, which specifies exactly this pair — and the old lighter
   lift (#f34a80) took it down to 3.44:1. A darker lift is 5.34:1.

   EVERY :hover IN THIS FILE SITS INSIDE @media (hover: hover). A touch screen
   keeps the hover state after the tap, so on the owner's phone and on the
   counter touchscreens a tapped room card lost its status colour to a white
   outline, the connection word stayed magenta, a ghost button stayed pink as
   if it were the thing to press, and the lighter primary was the state the
   screen usually showed. A device that can hover still gets every one. */
.btn-primary { color: #fff; background: var(--color-accent); border-color: transparent; }
@media (hover: hover) { .btn-primary:hover:not(:disabled) { background: var(--color-accent-600); } }
.btn-primary:active:not(:disabled) { background: var(--color-accent-700); }
/* The ghost: the border is the button. */
.btn-ghost {
  color: var(--color-text); background: transparent; border: var(--hair);
}
@media (hover: hover) { .btn-ghost:hover:not(:disabled) { border-color: var(--color-accent); color: var(--color-accent); } }
.btn-ghost:active:not(:disabled) { border-color: var(--color-accent-600); color: var(--color-accent-600); }
.btn-lg { font-size: var(--fs-lg); padding: var(--sp-4) var(--sp-5); min-height: 2.85rem; }

/* A word you can press. The whole design leans on these instead of buttons. */
.link {
  background: none; border: 0; padding: 0; font: inherit; font-size: var(--fs-base);
  line-height: 1; color: var(--color-neutral-400);
  transition: color 0.12s;
}
@media (hover: hover) { .link:hover { color: var(--color-accent); } }
.link:active { color: var(--color-accent-400); }
/* A text button has no box to grow, so it takes a thumb's height from
   padding and gives the space back with a negative margin — the layout does
   not move a pixel. At every size, not only under pointer:coarse: the
   counter touchscreens the .btn rule is written for cannot be seen by that
   query, and there "Freeze café" and "Take out for maintenance" were 16px
   tall. Vertically only: the trailing word in the header is flush with the
   edge of a 320px screen, and a negative inline margin there is a sideways
   scrollbar on the one screen that has no width to spare. */
.link { padding-block: 1.05rem; margin-block: -1.05rem; }
.link.quiet { color: var(--color-neutral-600); }
@media (hover: hover) { .link.quiet:hover { color: var(--color-neutral-400); } }
.link.on { color: var(--color-accent); }

/* ----------------------------------------------------------------- chrome -- */

.top { flex: 0 0 auto; }

.bar {
  display: flex; align-items: center; gap: var(--sp-5);
  padding: 0 var(--pad-x); height: var(--bar-h);
}
.brand { display: flex; align-items: baseline; gap: var(--sp-5); flex: 1 1 auto; min-width: 0; overflow: hidden; }
/* The wordmark, Kippo-style: bold monospace caps, wide. Arabic café names
   carry no tracking (they join) but keep the weight. The name may shrink, so
   that if it ever must give way it ends in an ellipsis: it could not (flex:
   0 0 auto), so its own text-overflow never fired and the brand's overflow
   sliced it instead — "PS CAFE MAAI", the D cut in half, in the first
   picture on the landing page. */
.brand strong {
  font-size: var(--fs-3xl); font-weight: 700; text-transform: uppercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  flex: 0 1 auto; min-width: 0;
}
html[lang="en"] .brand strong { letter-spacing: 0.16em; }
/* On a laptop-width counter the demo's header is full, and the wide tracking
   cost "PS CAFE MAADI" its last word — silently, since the brand clips. Half
   the tracking gives it back at 1366. At 1280, the counter's honest minimum,
   no tracking alone could: the English controls are a third wider than the
   Arabic, so below 1440 they close up — the gaps between them and the padding
   inside the surface switch — and the name gets the room back, with the
   offline count beside the connection word as well. */
@media (min-width: 620.02px) and (max-width: 1439.98px) {
  html[lang="en"] .brand strong { letter-spacing: 0.08em; }
  html[lang="en"] .controls { gap: var(--sp-4); }
  html[lang="en"] .roles button { padding-inline: var(--sp-2); font-size: var(--fs-sm); }
}
/* Inside the phone frame the English name, at its full tracking, needed
   152px of the 133 its row leaves beside the connection word and SIGN OUT.
   A quarter of the tracking still reads as the wordmark, and fits. */
@media (max-width: 620px) {
  html[lang="en"] body.embed .brand strong { letter-spacing: 0.04em; }
}
.brand .demo {
  font-size: var(--fs-sm); color: var(--color-neutral-600);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.controls {
  margin-inline-start: auto;
  display: flex; align-items: center; gap: var(--sp-6); font-size: var(--fs-base);
  flex-shrink: 0;
}
/* Each control is one unit — "شرح سريع" breaking over two lines on a phone
   read as two controls stacked. */
.controls > * { white-space: nowrap; }

/* Connection: a small dot and a word. The accent is the only colour the system
   has, so state is said in words rather than in red and green. Online is the
   plain white dot; offline is ash — the dot goes out, like a screen. */
.net {
  display: inline-flex; align-items: center; gap: var(--sp-3);
  background: none; border: 0; padding: 0;
  color: var(--color-neutral-400); font: inherit; font-size: var(--fs-base);
}
/* The same borrowed target as a text button (.link, above). */
.net { padding-block: 1.05rem; margin-block: -1.05rem; }
@media (hover: hover) { .net:hover { color: var(--color-accent); } }
.net .dot { width: 0.43rem; height: 0.43rem; border-radius: 50%; background: var(--color-online); flex: 0 0 auto; }
.net.off { color: var(--color-neutral-300); }
.net.off .dot { background: var(--color-neutral-600); }
.net .pill {
  font-size: var(--fs-xs); font-weight: 700; color: var(--color-neutral-300);
  font-variant-numeric: tabular-nums;
}

.sep { width: 1px; height: 1rem; background: var(--color-ash); flex: 0 0 auto; }

/* The surface switch is a segmented control, not five more loose words.
   It sat in the same row as the connection word, the tour, the language and
   the signed-in name, all in the same grey at the same size, and nothing said
   where the row of settings ended and the row of places began — on a phone it
   read as one run-on sentence. An ash outline around the group, and a white
   one around the surface you are on: Kippo's own outline vocabulary, no fill. */
.roles {
  display: flex; align-items: center; gap: 2px; padding: 2px;
  border: var(--hair-quiet); border-radius: var(--radius-sm);
}
.roles button {
  background: none; border: 1px solid transparent; border-radius: 8px;
  padding: var(--sp-1) var(--sp-3); font: inherit; font-size: var(--fs-base); line-height: var(--lh-ui);
  color: var(--color-neutral-500); white-space: nowrap;
}
@media (hover: hover) { .roles button:hover { color: var(--color-text); } }
.roles button.sel { color: var(--color-text); font-weight: 700; border-color: var(--color-text); }

/* The tab nav — Kippo's Navigation Link: uppercase monospace, no underline by
   default, and a 1px bottom border that turns magenta on the page you are on.
   The mark below IS that border; it stays a separate element so it can span
   the label rather than the button's whole column. */
.tabs {
  flex: 0 0 auto;
  display: flex; gap: var(--sp-7); padding: 2px var(--pad-x) 0;
  overflow-x: auto; scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }
.tabs button {
  display: flex; flex-direction: column; align-items: stretch; gap: var(--sp-3);
  background: none; border: 0; padding: var(--sp-2) 0 0;
  color: var(--color-neutral-500); font: inherit; font-size: var(--fs-lg); line-height: 1.4;
  font-weight: 700; text-transform: uppercase;
  white-space: nowrap;
}
@media (hover: hover) { .tabs button:hover { color: var(--color-text); } }
.tabs button.sel { color: var(--color-text); }
/* Flat, full-width, unskewed — Kippo is a wireframe, not a HUD. */
.tabs .bar-mark { width: 100%; height: 1px; background: transparent; }
.tabs button.sel .bar-mark { background: var(--color-accent); height: 2px; }
.tabs .count { color: var(--color-neutral-500); font-size: var(--fs-sm); font-weight: 400; }
/* A strip that scrolls sideways fades out at whichever end has more tabs past
   it (app.js marks which), so a hidden scrollbar is not a hidden tab. A mask,
   not a colour: the tabs go transparent into the void, nothing is painted. */
/* The reports' period and sub-tab rows take the same fade where a phone
   makes them scroll. */
.tabs.more-end, .mini-tabs.more-end { --fade-end: 1.5rem; }
.tabs.more-start, .mini-tabs.more-start { --fade-start: 1.5rem; }
.tabs.more-end, .tabs.more-start, .mini-tabs.more-end, .mini-tabs.more-start {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--fade-start, 0px), #000 calc(100% - var(--fade-end, 0px)), transparent);
  mask-image: linear-gradient(to right, transparent, #000 var(--fade-start, 0px), #000 calc(100% - var(--fade-end, 0px)), transparent);
}
html[dir="rtl"] .tabs.more-end, html[dir="rtl"] .tabs.more-start,
html[dir="rtl"] .mini-tabs.more-end, html[dir="rtl"] .mini-tabs.more-start {
  -webkit-mask-image: linear-gradient(to left, transparent, #000 var(--fade-start, 0px), #000 calc(100% - var(--fade-end, 0px)), transparent);
  mask-image: linear-gradient(to left, transparent, #000 var(--fade-start, 0px), #000 calc(100% - var(--fade-end, 0px)), transparent);
}

/* Offline is a sentence in a bordered band — no fill, like everything else. */
.banner {
  flex: 0 0 auto;
  padding: var(--sp-3) var(--pad-x); font-size: var(--fs-base); color: var(--color-accent);
  background: transparent;
  border-block: 1px solid var(--color-accent);
}
/* How much is waiting: a figure, so grey beside the warning's sentence, and
   set off from it rather than run on after its full stop. */
.banner .count { margin-inline-start: var(--sp-4); color: var(--color-neutral-300); white-space: nowrap; }

main {
  flex: 1; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  padding: var(--sp-6) var(--pad-x) var(--sp-6);
  display: flex; flex-direction: column; gap: var(--sp-5);
}

/* Most screens are a column of rows against the start edge, not a full-bleed
   sheet: the design caps them so a line of text never runs a metre wide. The
   caps are in rem, so they widen with the type rather than clipping it. */
.col { display: flex; flex-direction: column; min-width: 0; }
/* Lists and tables read across, then act: at 67rem a name sat at one edge
   of a counter monitor with its actions a thousand pixels away. */
.col-narrow { max-width: 56rem; }
.col-list { max-width: 46rem; }
.col-tight { max-width: 40rem; }

/* ------------------------------------------------------------------ heads -- */

.head { display: flex; align-items: baseline; gap: var(--sp-4); }
/* Section titles are Kippo display type: uppercase monospace, heavy, wide
   where the run is Latin, with a small flat square before them. */
.head h2 {
  margin: 0; font-size: var(--fs-3xl); font-weight: 700; text-transform: uppercase;
}
html[lang="en"] .head h2 { letter-spacing: var(--track-ui); }
/* The pip before a heading is ash, not magenta. A pink square on every
   heading on every screen was the accent spent as decoration — the exact
   wash that leaves it nothing to say when something needs pressing. */
.head h2::before, .sub-head::before {
  content: ""; display: inline-block; width: 6px; height: 6px;
  background: var(--color-neutral-600);
  margin-inline-end: var(--sp-3); vertical-align: 0.12em;
}
.head .count-note { font-size: var(--fs-md); color: var(--color-neutral-500); }
.head .btn, .head .link { margin-inline-start: auto; }
.helper {
  margin: var(--sp-1) 0 var(--sp-3); font-size: var(--fs-base); color: var(--color-neutral-500);
  max-width: 40rem;
}
.sub-head {
  font-size: var(--fs-md); font-weight: 500; color: var(--color-neutral-400);
  margin: var(--sp-8) 0 var(--sp-1);
}
/* A heading that OPENS its block starts flush; the space above is for one
   that follows content (price changes, past shifts). It was cancelled inline,
   one builder at a time, and the one that forgot — the payments column —
   sat a line below its twin with an empty band across the top of its card. */
.col > .sub-head:first-child, .chart-wrap > .sub-head:first-child { margin-top: 0; }

/* Plain numbers in a row — the stat boxes are gone. */
.stats { display: flex; align-items: baseline; gap: var(--sp-8); flex-wrap: wrap; }
.stats .stat {
  display: flex; align-items: baseline; gap: var(--sp-3);
  background: none; border: 0; padding: 0; font: inherit; cursor: pointer;
}
.stats .stat.sel b, .stats .stat.sel span { color: var(--color-accent); }
.stats .stat b { font-size: var(--fs-6xl); font-weight: 500; font-variant-numeric: tabular-nums; }
.stats .stat span { font-size: var(--fs-md); color: var(--color-neutral-500); }
.stats .stat.on b { color: var(--color-accent); }
.stats .stat.off b { color: var(--color-neutral-500); }
.stats .btn { margin-inline-start: auto; }

/* Stacked figures — the shift and the owner's KPI row. */
.figures { display: flex; gap: var(--sp-8); flex-wrap: wrap; }
/* Bottom-aligned, so a row of figures shares one label line even when one
   of them is a phrase rather than a number. */
.figure { display: flex; flex-direction: column; justify-content: flex-end; }
.figure .fval { display: flex; align-items: baseline; gap: var(--sp-3); flex-wrap: wrap; }
.figure b { font-size: 1.65rem; font-weight: 500; font-variant-numeric: tabular-nums; }
/* "All collected" is a verdict, not a figure: it is said at label weight in
   the figure's slot rather than shouted at the size of the money beside it. */
.figure b .phrase { font-size: var(--fs-2xl); font-weight: 500; color: var(--color-neutral-300); }
.figure > span:last-child { font-size: var(--fs-sm); color: var(--color-neutral-500); }
/* A rate is a price per something, not a third amount owed: its unit sits
   beside it, grey, the way the room card writes it. */
.figure b .unit { font-size: var(--fs-md); font-weight: 400; color: var(--color-neutral-500); }
/* The one figure that wears the accent is the owner's gap, and only while
   there is one (views-owner.js) — a signal to go and look, not a money
   colour; every other amount stays white. Declared once, here: it was
   written three times down this file, three places for a change to miss. */
.figure.on b { color: var(--color-accent); }
.figure .fdelta {
  font-style: normal; font-size: var(--fs-xs); font-weight: 500;
  color: var(--color-neutral-600); font-variant-numeric: tabular-nums;
  unicode-bidi: isolate; direction: ltr;
}
.figure .fdelta.up { color: var(--color-accent); }
.figures.lg .figure b { font-size: 1.88rem; }

/* ------------------------------------------------------------ rooms grid -- */

/* Cards size themselves off the type, so a counter screen shows five wide
   columns rather than four columns and a third of the monitor left empty. */
.grid {
  display: grid; gap: var(--sp-4); align-content: start;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr));
}

/* The grid is grouped by status now — occupied first, then free, with any
   room under maintenance dimmed at the END of the free group rather than
   under a third head: that extra row pushed a 1280×900 counter into
   scrolling whenever a room was busy. The headers live INSIDE the one grid,
   spanning its full width, so the column arithmetic (and the tests that
   assert it) see a single grid. */
.sect-head {
  grid-column: 1 / -1;
  display: flex; align-items: baseline; gap: var(--sp-3);
  font-size: var(--fs-lg); font-weight: 500; color: var(--color-neutral-400);
  margin-top: var(--sp-2);
}
.sect-head:first-child { margin-top: 0; }
.sect-head.on { color: var(--color-accent); }
.sect-head .cnt { font-size: var(--fs-sm); font-weight: 400; color: var(--color-neutral-600); }

/* The Outlined Card, which is Kippo's dominant pattern and the reason this
   grid reads from two metres: black fill, a 1px border, 15px radius, no
   shadow and no colour wash. The BORDER is the card, and the border is also
   where status lives. */
.room {
  text-align: start; font: inherit; color: var(--color-text);
  display: flex; flex-direction: column; gap: var(--sp-2);
  background: var(--color-bg);
  border: 1px solid var(--color-ash); border-radius: var(--radius-lg);
  padding: var(--sp-4) var(--sp-5); min-height: var(--room-h);
  transition: border-color 0.12s, background 0.12s;
}
@media (hover: hover) { .room:hover:not([disabled]) { border-color: var(--color-text); } }
/* Four outline weights, all inside Kippo's own palette — free is the plain
   white wireframe, in use is the pink power-up, promised is Terminal Cyan,
   maintenance is ash and dim. No fills: a card that fills is a different
   design system. */
.room.available { border-color: var(--ok); }
.room.available .state { color: var(--ok); }
.room.occupied { border-color: var(--busy); }
.room.reserved, .room.held { border-color: var(--resv); }
.room.reserved .state, .room.held .state { color: var(--resv); }
.room.maintenance { border-color: var(--color-ash); opacity: 0.62; }
/* A frozen café's free room: it cannot start a session, so it gives up the
   white "free" outline and dims like maintenance — the grid stops saying yes
   before the sheet has to say no. Rooms in use stay live to be closed. */
.room.frozen { border-color: var(--color-ash); }
/* The stat filters dim what they exclude - the grid keeps its shape, so the
   room staff know as "third from the right" stays third from the right. */
.room.dim { opacity: 0.3; }
/* A press is the one moment a card is allowed to fill, and it fills with
   Carbon, not with accent. */
.room:active:not([disabled]) { background: var(--color-carbon); }
.room[disabled] { opacity: 0.45; cursor: not-allowed; }

/* The small mark that says "in use" before you have read anything — the
   power-up, flat and square. */
.room .mark {
  width: 1.53rem; height: 2px; border-radius: 0;
  background: var(--busy); margin-bottom: var(--sp-1);
}
/* The state word keeps the card's top corner whatever the name does: a long
   name ("الواقع الافتراضي" on a phone card) wraps inside its own box rather
   than pushing the state down a line, so every card's corner reads the same. */
.room-head { display: flex; align-items: baseline; gap: var(--sp-3); min-width: 0; }
/* "Inside its own box" includes a word too long for the box: a phone card
   drew "BILLIARD" straight into "Available" beside it, because a box can
   wrap only between words. The word breaks where it must instead. */
.room .name {
  font-size: var(--fs-2xl); font-weight: 700; min-width: 0; text-transform: uppercase;
  overflow-wrap: break-word; hyphens: auto;
}
.room .type { flex: 0 0 auto; }
/* The type badge: a ghost outline carrying the owner's own words. The hashed
   hue is retired (see typeStyle in ui.js) — one colour in this system, and a
   room's type is not it. --ty is still honoured if a café ever gets it back. */
.room .type {
  font-size: var(--fs-xs); font-weight: 700; color: var(--ty, var(--color-neutral-300));
  background: transparent;
  border: 1px solid var(--ty, var(--color-ash));
  padding: 1px 6px; border-radius: var(--radius-sm);
}
/* A promise is coming: the tile says when, and says it in accent inside the
   last half hour — the one collision the engine's locks cannot prevent. */
.room .upnext { font-size: var(--fs-sm); color: var(--color-neutral-500); }
.room .upnext.warn { color: var(--color-accent); }

.room .state {
  margin-inline-start: auto; font-size: var(--fs-sm); color: var(--color-neutral-500);
  flex: 0 0 auto;
}
.room.occupied .state { color: var(--busy); }
/* The running clock is the loudest thing on the card, and it is the one place
   the accent is allowed to be big — a session in progress IS the power-up. */
.room .timer {
  font-size: 2rem; font-weight: 700; font-variant-numeric: tabular-nums;
  color: var(--busy); letter-spacing: 0.06em; line-height: 1.15;
}
/* How far into a typical two-hour session this room is — read at a glance
   from across the counter, before any number is. */
.room .prog {
  display: block; height: 2px; border-radius: 0;
  background: var(--color-ash); overflow: hidden;
}
.room .prog b {
  display: block; height: 100%;
  background: var(--busy);
}
.room .line {
  display: flex; justify-content: space-between; gap: var(--sp-4);
  margin-top: auto;
  font-size: var(--fs-base); color: var(--color-neutral-500);
}
/* The label may wrap on a narrow card; the money never splits "10" from "ج". */
.room .line > span { min-width: 0; }
.room .line b { color: var(--color-text); font-weight: 500; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* The customer's line sits on the money line rather than under the clock —
   the two are read together, and the name no longer stands in for the
   money's label. Not ".who": that is the header's signed-in name, which a
   phone hides. */
.room .cust {
  margin-top: auto; font-size: var(--fs-md); color: var(--color-neutral-500);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.room .cust + .line { margin-top: 0; }
.room .price { margin-top: auto; font-size: 1.65rem; font-weight: 500; line-height: 1.2; }
.room .price span { font-size: var(--fs-md); font-weight: 400; color: var(--color-neutral-500); }
.room .seats { font-size: var(--fs-md); color: var(--color-neutral-500); }
.room .muted { margin-top: auto; font-size: var(--fs-md); color: var(--color-neutral-500); }
/* A promise on this room later today — the counter should notice it, and
   there is nothing to press. Inside the last half hour it escalates to the
   accent below, because by then it IS about to need doing. */
.room .next { font-size: var(--fs-sm); color: var(--color-neutral-500); }
.room:not(.occupied) .next { margin-top: var(--sp-1); }
.room .next.soon { color: var(--color-accent); }
.room .lift { font-style: normal; font-size: var(--fs-xs); color: var(--color-neutral-600); }
/* Work on this room still in the outbox. A line of its own under the bar:
   on the customer's line it was ellipsed away behind the name. Grey, because
   it is a fact about the queue and nothing to press. */
.room .queued { font-size: var(--fs-xs); color: var(--color-neutral-500); }

/* ------------------------------------------------------------------ rows -- */

/* One list pattern for reservations, the waitlist and past shifts: a time
   column, the body, then the actions — separated by fading rules.

   The column is sized for the WIDEST time this clock can print, which is not
   the obvious one. Hours are tabular so every digit is equal, but a two-digit
   hour is one glyph wider than a single-digit one, and the Arabic meridiem is
   not one width either — "ص" is appreciably wider than "م" in Tajawal. So the
   worst case is "12:00 ص" (measured 123px at the counter, against the 108px
   this column used to be), not the "11:45 م" it was tuned against. Anything
   between 10 and 12 o'clock wrapped onto a second line, which is a quarter of
   the day, and it went unnoticed because the demo seeds its reservations
   relative to now — the bug was only on screen when somebody looked late.

   And the row sits on the name's baseline, so the time reads along the same
   line as the name it belongs to however far the body under it wraps:
   centred, a phone's two-line meta or the customer's three-line booking
   floated the time between lines. The actions stay centred on the row; a
   flat row is one line and stays centred too. */
.row {
  display: grid; grid-template-columns: 7.2rem 1fr auto;
  gap: var(--sp-6); align-items: baseline; padding: var(--sp-5) 0;
}
.row > .row-actions { align-self: center; }
.row.flat { grid-template-columns: 1fr auto; padding: var(--sp-4) 0; align-items: center; }
.row-when { display: flex; flex-direction: column; }
/* nowrap at every size, not just the phone: without it a column that is one
   pixel short drops the meridiem under the number and nothing complains. With
   it, the overflow is visible and the spill test catches it. */
/* A time is a quiet figure (section 6, 3b): grey, so the next one due — white
   and bold, at the foot of this file — is the only loud time in the list. It
   inherited white, and the standout was a weight step nobody could see. */
.row-when b {
  font-size: 1.4rem; font-weight: 500; font-variant-numeric: tabular-nums;
  white-space: nowrap; color: var(--color-neutral-400);
}
.row-when span { font-size: var(--fs-sm); color: var(--color-neutral-500); }
.row-body { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
.row-title { display: flex; align-items: baseline; gap: var(--sp-4); flex-wrap: wrap; }
.row-title b { font-size: var(--fs-2xl); font-weight: 500; }
.row-title .type { font-size: var(--fs-xs); color: var(--color-neutral-500); }
/* A kept promise speaks in the promise's own voice — cyan, as on the grid —
   and only a booking still waiting on the counter to confirm it asks in the
   accent, because that one is a thing to do. */
.row-title .state { font-size: var(--fs-sm); color: var(--resv); }
.row-title .state.pending { color: var(--color-accent); }
.row-title .state.quiet { color: var(--color-neutral-500); }
.row-meta { font-size: var(--fs-base); color: var(--color-neutral-500); }
.row-meta .ltr {
  unicode-bidi: isolate; font-variant-numeric: tabular-nums;
}
/* A customer's number is a tel: link, so a phone dials it in one tap — and
   it stays a quiet grey figure (section 6, 3b), not a blue underline. One
   token: its groups are spaced for reading, not for wrapping. */
a.tel { color: inherit; text-decoration: none; white-space: nowrap; }
@media (hover: hover) { a.tel:hover { color: var(--color-text); } }
.row-actions { display: flex; gap: var(--sp-3); align-items: center; flex-wrap: wrap; }
.row-end { font-size: var(--fs-md); color: var(--color-neutral-500); font-variant-numeric: tabular-nums; }
/* A drawer that came up short or over. It is the number the owner reads and
   cannot press — the shift is already closed. */
.row-end.on { color: var(--color-text); font-weight: 700; }
.row.muted b, .row.muted .row-meta { color: var(--color-neutral-600); }

/* ----------------------------------------------------------------- tables -- */

/* Not a <table>: a grid, so the fading rule can span the whole row rather than
   stopping at a cell edge. */
.tbl { display: flex; flex-direction: column; }
.tbl-row {
  display: grid; gap: var(--sp-5); align-items: center; padding: var(--sp-4) 0;
}
.tbl-head {
  padding: var(--sp-3) 0; font-size: var(--fs-xs); color: var(--color-neutral-600);
}
.tbl-row b { font-size: var(--fs-xl); font-weight: 500; }
.tbl-row .cell { font-size: var(--fs-base); color: var(--color-neutral-500); }
.tbl-row .cell.strong { color: var(--color-text); }
.tbl-row .acts { display: flex; gap: var(--sp-5); justify-content: flex-end; }
.tbl-row.muted b, .tbl-row.muted .cell { color: var(--color-neutral-600); }
.tbl-row .flagged { font-size: var(--fs-sm); color: var(--color-neutral-500); }
.tbl-row .with-note { display: flex; align-items: baseline; gap: var(--sp-4); min-width: 0; }

/* Three columns only, per the modernisation pass: an identity block (the name
   with a muted meta line under it), the one value that matters, and the
   actions as words. Everything else moved into the meta line. */
.cols-rooms { grid-template-columns: 1fr 10rem auto; }
.cols-menu { grid-template-columns: 1fr 7.9rem auto; }
.cols-staff { grid-template-columns: 1fr 8.6rem auto; }
.cols-expense { grid-template-columns: 1fr 8.6rem auto; }
.cols-note { grid-template-columns: 1fr auto; }
.cols-note .body {
  display: block; font-size: var(--fs-lg); color: var(--color-text);
  margin-block: 2px; white-space: pre-wrap;
}
.mark-dot {
  display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  background: var(--color-accent-500); margin-inline-end: 2px;
}
/* An unread note is the standout of the list, so its kind is bold beside the
   read ones' regular — weight, never a fill — and the dot is the thing still
   to do, which is why it alone carries the accent. */
.tbl-row.cols-note.unread b { font-weight: 700; }
.tbl-row.cols-note:not(.unread) b { color: var(--color-neutral-400); }
.tbl-row.cols-note:not(.unread) .body { color: var(--color-neutral-300); }
.cols-note .with-note { flex-wrap: wrap; row-gap: 2px; }

/* The counter's rooms head carries two trailing words — the note and the
   counter sale — and only the first takes the push to the far edge. On a
   phone the three no longer fit beside the title and COUNTER SALE fell to a
   line of its own, so the room count gives way there: the group heads under
   it ("Available · 7") already count the rooms, and the tiles are the rooms. */
.head .link + .btn { margin-inline-start: 0; }
@media (max-width: 620px) { .rooms-head .count-note { display: none; } }

/* ---------------------------------------------------------------- reviews -- */
/* Stars are text glyphs in the accent — this system has no gold, and a rating is
   exactly the kind of rare, earned thing the power-up colour is for. Still no
   icon font. The unlit ones sit a step above ash: at #333 on the void the
   empty stars of a four were not there at all, and a four read as a four out
   of four. */
.stars { color: var(--gold); letter-spacing: 0.1em; white-space: nowrap; }
.stars .off { color: var(--color-neutral-700); }

/* The average is a figure, not money, so it keeps its size and steps down to
   the quiet grey every other figure here wears (CLAUDE.md section 6, 3b). */
.rating-hero { display: flex; align-items: center; gap: var(--sp-4); }
.rating-hero > b { font-size: 2.4rem; font-weight: 500; font-variant-numeric: tabular-nums; color: var(--color-neutral-400); }
.rating-hero .meta { display: block; font-size: var(--fs-sm); color: var(--color-neutral-500); }

.histo { display: flex; flex-direction: column; gap: 5px; max-width: 22rem; margin-top: var(--sp-3); }
.histo-row { display: flex; align-items: center; gap: var(--sp-3); font-size: var(--fs-sm); color: var(--color-neutral-500); }
.histo-row .count { min-width: 1.4rem; text-align: start; }
.histo-bar {
  flex: 1; height: 3px; border-radius: 0; overflow: hidden;
  background: var(--color-ash);
}
/* Counts, so grey — magenta acts, and a bar of a histogram is nothing to
   press. The most common rating is the white bar among the grey ones. */
.histo-bar span { display: block; height: 100%; background: var(--color-neutral-600); }
.histo-bar span.top { background: var(--color-text); }

.star-pick { display: flex; gap: var(--sp-2); }
/* A thumb's target each, and an unlit star an edge a person can see: these
   are the controls, and ash at 1.66:1 was below what a control needs. */
.star-pick .star {
  font: inherit; font-size: 1.8rem; line-height: 1; padding: var(--sp-2);
  min-width: 2.9rem; min-height: 2.9rem;
  background: none; border: 0; cursor: pointer;
  color: var(--color-neutral-600);
}
.star-pick .star.on { color: var(--gold); }

.review { padding-block: var(--sp-4); border-bottom: 1px solid var(--color-divider); }
.review-head { display: flex; align-items: baseline; gap: var(--sp-3); flex-wrap: wrap; }
.review-head b { font-size: var(--fs-lg); }
.review-head .type, .review-head i { font-style: normal; font-size: var(--fs-sm); color: var(--color-neutral-500); }
.review-head .flagged { font-size: var(--fs-sm); color: var(--color-neutral-500); }

/* The customer's own review, and the one way to write or change it. */
.reviews-tab .sub-head { margin-top: var(--sp-6); }
.review.mine { border-bottom: 0; padding-bottom: var(--sp-2); }
.mine-acts { display: flex; margin: var(--sp-2) 0 var(--sp-5); }
.reviews-admin .rating-hero, .reviews-tab .rating-hero { margin-top: var(--sp-3); }
.rv-filters { margin-top: var(--sp-5); }
/* Six chips on one row of a phone — "all" and the five star counts — rather
   than the "1 ★" left alone on a second. */
@media (max-width: 620px) { .rv-filters { gap: var(--sp-2); } .rv-filters .chip { padding-inline: var(--sp-3); } }

/* The café's rating above its rooms: the way to its reviews, so a button —
   drawn as a line of text, the stars the only colour in it. */
.rating-line {
  display: inline-flex; align-items: baseline; gap: var(--sp-3); align-self: flex-start;
  background: none; border: 0; padding: var(--sp-2) 0; margin-bottom: var(--sp-3);
  font: inherit; font-size: var(--fs-base); color: var(--color-neutral-500); cursor: pointer;
}
.rating-line b { color: var(--color-neutral-300); font-weight: 700; font-variant-numeric: tabular-nums; }
@media (hover: hover) { .rating-line:hover .meta { color: var(--color-text); } }
.review-body { margin: var(--sp-1) 0 0; font-size: var(--fs-lg); white-space: pre-wrap; }
.review-foot { margin-top: var(--sp-1); }
.review-foot .like.on { color: var(--color-accent); }
.review-foot .meta { font-size: var(--fs-sm); color: var(--color-neutral-500); }

/* One thin line, two shares: the app's part in accent, the rest in the
   divider's grey. Plain numbers carry the detail underneath. */
.split-bar {
  height: 3px; border-radius: 0; overflow: hidden;
  background: var(--color-ash);
  margin-block: var(--sp-2) var(--sp-3);
}
.split-bar span { display: block; height: 100%; width: var(--split, 0%); background: var(--color-accent); }
.tbl-row .id { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.tbl-row .id .meta { font-size: var(--fs-sm); color: var(--color-neutral-500); }
.tbl-row.muted .id .meta { color: var(--color-neutral-600); }
/* Money tracks wide enough for "EGP 1,132.50" on one line: at 6.3rem the
   amounts broke under their unit while the table stopped at 46rem with a
   third of the monitor empty beside it (it takes the 56rem cap now). */
.cols-report { grid-template-columns: minmax(0, 1fr) 4rem 4rem 7rem 6.5rem 6.5rem 7.5rem; }
@media (min-width: 621px) {
  .cols-report .cell, .cols-report.tbl-head span { white-space: nowrap; }
  /* A room under maintenance keeps its name on one line; the flag may drop
     under it. */
  .cols-report .with-note { flex-wrap: wrap; row-gap: 0; }
  .cols-report .with-note b { white-space: nowrap; }
}
/* The time column holds "11:00 pm" — the evening is when a café is busiest,
   so a column tuned to "7:12 am" broke a quarter of a month's rows (the
   lesson .row learned, above). Over more than a day the date sits under it,
   quiet, the way a reservation's does. */
.cols-sess { grid-template-columns: 5.6rem 1fr 5.5rem 5.5rem 6.2rem minmax(0, 1.35fr); }
.cols-sess .cell.num { white-space: nowrap; }
.sess-when .d { display: block; font-size: var(--fs-sm); color: var(--color-neutral-600); }
/* The row the gap comes from, in the gap figure's own accent. */
.paid-short { color: var(--color-accent); font-weight: 700; white-space: nowrap; }
/* A discount on the row is a record, not a question: allowed, under the
   café's limit, and named — so it is quiet grey, where the short payment
   beside it takes the accent. */
.disc-note { display: inline-block; color: var(--color-neutral-500); font-size: var(--fs-sm); }

/* The reports' period row: text tabs plus a real date field, one line. */
.report-tabs { display: flex; align-items: center; gap: var(--sp-6); flex-wrap: wrap; }
/* The date is our words in an outline — "يوم معين", then the day chosen, in
   the app's own format — with the browser's date field laid over it unseen
   (views-owner.js), so its "mm/dd/yyyy" never shows. */
.day-pick {
  position: relative; display: inline-flex; align-items: center; gap: var(--sp-2);
  margin: 0; min-height: 0; padding: var(--sp-1) var(--sp-3);
  font-size: var(--fs-base); line-height: var(--lh-ui); color: var(--color-neutral-400);
  background: transparent; border: 1px solid var(--color-divider);
  border-radius: var(--radius-md); cursor: pointer;
}
.day-pick input {
  position: absolute; inset: 0; width: 100%; height: 100%; min-height: 0;
  padding: 0; border: 0; opacity: 0; cursor: pointer;
}
.day-pick:has(input:focus-visible) { outline: 2px solid var(--color-accent-400); outline-offset: 2px; }
.day-pick .cal { flex: 0 0 auto; }
.day-pick.sel { color: var(--color-accent); border-color: var(--color-accent); }
/* The expense sheet's day (29-9-2026): the same box drawn as a field — the
   width, height and white hairline of the fields above it, the day picked in
   the field's own white. It turns magenta only when the day is refused, as a
   field that answered wrong does. */
.day-pick.sheet-day {
  display: flex; width: 100%; min-height: 2.8rem; padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-lg); color: var(--color-text); border: var(--hair);
}
.day-pick.sheet-day .cal { color: var(--color-neutral-400); }
.day-pick.sheet-day:has(input[aria-invalid="true"]) { border-color: var(--color-accent); }
.mini-tabs.rtabs { margin-top: var(--sp-1); }
.mini-tabs .count { color: var(--color-neutral-500); font-size: var(--fs-xs); }
.cols-paylist { grid-template-columns: 4.3rem 1fr 6.4rem minmax(0, 1.6fr); }

/* Two narrow breakdowns share a row on a wide screen instead of stacking —
   this is what keeps the reports on one counter screen with the tour bar up. */
/* Stacked on a phone, the second block keeps a section's distance from the
   first: its heading starts flush now, so the row gap is the space. */
.duo {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 24rem), 1fr));
  gap: var(--sp-7) var(--sp-8); align-items: start;
}

/* One scheduled-rate window on the room sheet: from, to, price, remove. */
.sched-row {
  display: grid; grid-template-columns: auto 1fr auto 1fr auto 1fr auto;
  gap: var(--sp-3); align-items: center; margin-top: var(--sp-2);
}
.sched-row label { margin: 0; }
.sched-row input { min-height: 2.3rem; }
.sched-row + .chip, .sched-row + button.chip { margin-top: var(--sp-3); }

/* ------------------------------------------------------------------ hero -- */

/* The number the owner opens the app for. Kippo puts gradients on decorative
   backdrops and NEVER on a card fill, so the chamfered red panel is gone: this
   is an outlined card with a magenta border — the one card in the app whose
   outline is the accent, which is exactly how Kippo rations it. The number
   itself is the display face. */
.hero {
  display: flex; flex-direction: column; gap: var(--sp-1);
  background: var(--color-bg);
  border: 1px solid var(--color-accent);
  border-radius: var(--radius-lg);
  padding: var(--sp-5) var(--sp-6);
}
.hero .label {
  font-size: var(--fs-sm); color: var(--color-neutral-500); text-transform: uppercase;
}
.hero .amount { line-height: 1.05; }
.hero .amount b {
  font-size: var(--fs-11xl); font-weight: 700; font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}
.hero .amount span { font-size: var(--fs-3xl); color: var(--color-neutral-500); }
.hero .delta { font-size: var(--fs-base); color: var(--color-neutral-500); margin-top: var(--sp-1); }
/* Ahead of yesterday takes the accent; behind it goes quiet. Both arrows used
   to be the same magenta, which meant the one number the owner opens the app
   to check said the same thing on a good day and a bad one. Accent and grey,
   not green and red — the direction is already in the arrow and the sign, so
   the colour only has to make "good" worth a glance. */
.hero .delta .trend { color: var(--color-neutral-500); }
.hero .delta .trend.up { color: var(--color-accent); }
/* Against yesterday, beside the big number — a word and a signed percent,
   accent when the day is ahead, quiet when it is behind. */
.hero .amount .trend {
  font-size: var(--fs-md); font-weight: 400; color: var(--color-neutral-500);
  margin-inline-start: var(--sp-3); vertical-align: 0.55em;
}
/* A day ahead of yesterday lights up in the accent — this system has no
   green, and "ahead" is exactly what a power-up is for. */
.hero .amount .trend.on { color: var(--color-accent); }
.hero.sm .amount b { font-size: 2.6rem; }
.hero.sm .amount span { font-size: var(--fs-2xl); }

.hero-row { display: flex; align-items: flex-end; gap: var(--sp-8); flex-wrap: wrap; }
.spark-wrap {
  display: flex; flex-direction: column; gap: var(--sp-1); padding-bottom: var(--sp-1);
  flex: 1 1 auto; min-width: 16rem; max-width: 23rem;
}
.spark { display: flex; align-items: flex-end; gap: 4px; height: 3.8rem; }
.spark-bar {
  flex: 1 1 0; min-width: 0; border-radius: 0; min-height: 2px;
  background: var(--color-ash);
}
/* Today is the only bar with colour in it — the rest are ash. */
.spark-bar.today { background: var(--color-accent); }
/* The week's best day is marked in the figures block at the foot of this
   file, by weight and a white bar. Today is the day you are acting in and
   keeps the accent; the best day is only worth knowing. */

/* Each bar under its weekday's name — the same flex shares, so the columns
   line up whatever the labels' widths. */
.spark-days { display: flex; gap: 4px; }
.spark-days span {
  flex: 1 1 0; min-width: 0; text-align: center;
  font-size: var(--fs-3xs); color: var(--color-neutral-600);
  white-space: nowrap; overflow: hidden;
}
.spark-days span.today { color: var(--color-accent); }
/* Tajawal's joined strokes thin out at this size in the 46% grey, and the
   weekday names ran together on a phone: the Arabic shell's smallest
   captions take the next grey up. */
html[lang="ar"] .spark-days span:not(.today),
html[lang="ar"] .tbl-row .cell[data-l]::before { color: var(--color-neutral-500); }
/* Each day is written twice, the printed name and the spoken short one
   (views-owner.js), and the width picks. On a phone the full Arabic names —
   الأربعاء, الخميس — filled their ~34px columns at 11.4px and could go no
   larger; the short ones (أربع, خميس) leave room for a step past 12px, the
   size the real panel's week chart reads at on the same screen. */
.spark-days i { font-style: normal; }
.spark-days .wd-short { display: none; }
@media (max-width: 620px) {
  /* A short name can still be a hair wider than a 320px phone's column
     (خميس is 36px at this size): it spills evenly into the gaps either side,
     under its own bar, rather than losing its last letter to a clip. */
  .spark-days span { font-size: 0.78rem; display: flex; justify-content: center; overflow: visible; }
  .spark-days .wd-long { display: none; }
  .spark-days .wd-short { display: inline; }
}

.hero-row .aside {
  font-size: var(--fs-base); color: var(--color-neutral-500); padding-bottom: var(--sp-1);
}
.hero-row .aside .on { color: var(--color-accent); }

/* Revenue by type, and any other label/value list. */
.breakdown { display: flex; flex-direction: column; }
.breakdown .brow {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: var(--sp-4); padding: var(--sp-3) 0; font-size: var(--fs-lg);
}
.breakdown .brow .qty { font-size: var(--fs-sm); color: var(--color-neutral-600); }
.breakdown .brow b { font-size: var(--fs-lg); font-weight: 500; font-variant-numeric: tabular-nums; }
.breakdown .brow.dim > span { color: var(--color-neutral-400); }
.breakdown .brow.total { padding: var(--sp-4) 0; font-weight: 500; }
.breakdown .brow.total b { font-size: var(--fs-2xl); }

/* Mini text tabs — the period switch on the reports screen. */
.mini-tabs { display: flex; gap: var(--sp-6); flex-wrap: wrap; }
.mini-tabs button {
  display: flex; flex-direction: column; align-items: stretch; gap: var(--sp-2);
  background: none; border: 0; padding: 0; font: inherit; font-size: var(--fs-md);
  line-height: 1.4; color: var(--color-neutral-500);
  font-weight: 700; text-transform: uppercase;
}
@media (hover: hover) { .mini-tabs button:hover { color: var(--color-text); } }
.mini-tabs button.sel { color: var(--color-text); }
.mini-tabs .bar-mark { width: 100%; height: 1px; background: transparent; }
.mini-tabs button.sel .bar-mark { background: var(--color-accent); height: 2px; }

/* ----------------------------------------------------------------- chart -- */

.chart-wrap { display: flex; flex-direction: column; gap: var(--sp-2); max-width: 46rem; }
/* The dashboard's seven days: the same chart, smaller and quieter. The
   chart box carries its own headroom so the tallest bar's value never
   collides with the hero card above it. */
.chart-wrap.week { max-width: 28rem; --chart-h: 6.4rem; margin-top: var(--sp-5); }
.chart-wrap.week .chart { padding-top: 1.15rem; }
/* Weekday names under the seven bars — the ask of 25-8-2026. */
.chart-wrap.week .chart-labels span { max-width: none; font-size: var(--fs-3xs); white-space: nowrap; overflow: hidden; }
/* Headroom for the value that rides on the tallest bar: a 100% bar plus its
   label overflows the box upward, and without this strip it lands on the
   section heading above the chart. The same strip the week chart already
   carried, now on every chart. */
.chart {
  display: flex; align-items: flex-end; gap: var(--sp-2); height: var(--chart-h);
  padding-top: 1.15rem;
}
.chart-cell {
  flex: 1; max-width: 2.2rem; height: 100%;
  display: flex; flex-direction: column; justify-content: flex-end;
  align-items: center; gap: 2px;
}
.chart-cell .chart-bar { width: 100%; flex: 0 0 auto; }
.chart-val {
  font-size: var(--fs-3xs); color: var(--color-neutral-500);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.chart-val.today { color: var(--color-accent); }
.chart-bar {
  flex: 1; max-width: 2.2rem; background: var(--color-ash);
  border-radius: 0; min-height: 2px;
}
/* The peak bar is the power-up. Flat fill — gradients are atmosphere in
   this system, never chrome. */
.chart-bar.today { background: var(--color-accent); }
.chart-labels {
  display: flex; gap: var(--sp-2); font-size: var(--fs-3xs);
  color: var(--color-neutral-600); font-variant-numeric: tabular-nums;
}
/* A label shrinks exactly as its bar does — same count, gap, flex and cap in
   both rows — so day n always sits under bar n. Without min-width a label
   could not go below its text, and on a phone a month of them outgrew the
   bars: the 12th's best day read "08", today read "19", and the overflow
   dragged the whole reports screen sideways. A label wider than its bar
   spills evenly both ways from the bar's centre rather than off one side. */
.chart-labels span {
  flex: 1; min-width: 0; max-width: 2.2rem;
  display: flex; justify-content: center; white-space: nowrap;
}
.chart-labels span.today { color: var(--color-accent); }
/* The bars need the same floor: a labelled bar's "828" set its column's
   minimum, so the best day's column stayed wide while the rest shrank and
   every bar after it slid off its day. The value spills evenly instead. */
.chart-cell { min-width: 0; }
/* No chart can widen the screen it sits in. */
.chart-wrap { min-width: 0; max-width: min(46rem, 100%); }
@media (max-width: 620px) {
  /* A phone's month is ~11px a bar, too narrow to print every day: the 1st,
     every fifth and today are printed, and a fifth that would touch today
     gives way to it. The bars close up to a hairline gap so the labels that
     remain have room either side. */
  .chart, .chart-labels { gap: 2px; }
  .chart-labels span:not(.tick):not(.today),
  .chart-labels span.tick:has(+ .today),
  .chart-labels span.today + span.tick { visibility: hidden; }
}

/* ---------------------------------------------------------------- sheets -- */

/* Not part of the handoff's nine screens; built to the same system so the
   counter does not change language halfway through a sale. */
.scrim {
  position: fixed; inset: 0; z-index: 10;
  background: rgba(0, 0, 0, 0.78);
  animation: fade 0.14s ease-out;
}
.sheet {
  position: fixed; inset-inline: 0; bottom: 0; z-index: 11;
  --sheet-pb: 1.29rem;
  /* A sheet steps forward the way every other surface here does: a white
     hairline on the void. It used to be a Carbon slab, which is the one
     filled panel in a system whose rule is "the border is the card", and
     it read as a stock modal dropped on top of the product. The scrim
     behind it dims the page, so the outline alone carries it. */
  background: var(--color-bg);
  border: var(--hair); border-bottom: 0;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  padding: var(--sp-6) var(--sheet-px) calc(var(--sheet-pb) + env(safe-area-inset-bottom));
  max-height: 88dvh; overflow-y: auto; overscroll-behavior: contain;
  max-width: 45rem; margin: 0 auto;
  animation: rise 0.18s cubic-bezier(0.22, 0.8, 0.3, 1);
}
.sheet h2 {
  margin: 0 0 var(--sp-4); font-size: var(--fs-4xl); font-weight: 700;
  text-transform: uppercase;
}
html[lang="en"] .sheet h2 { letter-spacing: var(--track-ui); }
.sheet h3 {
  margin: var(--sp-5) 0 var(--sp-2); font-size: var(--fs-sm); font-weight: 700;
  color: var(--color-neutral-400); text-transform: uppercase;
}
.sheet .sub { margin: calc(-1 * var(--sp-3)) 0 var(--sp-4); font-size: var(--fs-base); color: var(--color-neutral-500); }

/* On a wide screen a sheet is a dialog, not a drawer. Pinned to the bottom
   edge of a 1280px monitor its lower border ran off the screen and it read
   as a panel cut in half. Centred, it has all four edges. The walkthrough
   keeps the drawer: its bar owns the bottom and the sheet sits on it. */
@media (min-width: 900px) {
  body:not(.touring) .sheet {
    inset-block: 0; margin: auto; height: fit-content;
    max-height: calc(100dvh - 3rem);
    border-bottom: var(--hair); border-radius: var(--radius-lg);
  }
}

@keyframes fade { from { opacity: 0; } }
@keyframes rise { from { transform: translateY(12px); opacity: 0; } }

label {
  display: block; font-size: var(--fs-sm); margin: var(--sp-4) 0 var(--sp-2);
  color: var(--color-neutral-400); text-transform: uppercase;
}
label i { font-style: normal; color: var(--color-neutral-600); }
/* Kippo's input outline, as the reference writes it: Ghost White, the same
   1px white every other element here is drawn in, and magenta while you are
   in it. It was ash (#333, 1.66:1 on the void) and turned white only on
   hover — which a touchscreen never has — so an empty phone or password field
   was a rectangle nobody could quite see, below the 3:1 a field's edge needs. */
input {
  width: 100%; font: inherit; font-size: var(--fs-2xl); min-height: 2.8rem;
  padding: var(--sp-3) var(--sp-4);
  color: var(--color-text); caret-color: var(--color-accent);
  background: transparent;
  border: var(--hair); border-radius: var(--radius-md);
}
input:focus { outline: none; border-color: var(--color-accent); }
input::placeholder { color: var(--color-neutral-600); }

/* The note sheet's one long field — an input that grew, nothing more. */
textarea {
  width: 100%; font: inherit; font-size: var(--fs-2xl);
  padding: var(--sp-3) var(--sp-4); resize: vertical;
  color: var(--color-text); caret-color: var(--color-accent);
  background: transparent;
  border: var(--hair); border-radius: var(--radius-md);
}
textarea:focus { outline: none; border-color: var(--color-accent); }
textarea::placeholder { color: var(--color-neutral-600); }

/* Segmented choices — categories, payment methods, pricing units. Ghost
   outline buttons, exactly; the chosen one is the only filled thing in the
   row. */
.chip {
  background: none; color: var(--color-neutral-400); font: inherit; font-size: var(--fs-md);
  font-weight: 700; text-transform: uppercase;
  border: var(--hair-quiet); border-radius: var(--radius-md);
  padding: var(--sp-3) var(--sp-4); white-space: nowrap;
  transition: color 0.12s, border-color 0.12s, background 0.12s;
}
@media (hover: hover) { .chip:hover:not(:disabled) { color: var(--color-text); border-color: var(--color-text); } }
.chip.sel { color: var(--color-accent); border-color: var(--color-accent); }
.chip { min-height: 2.6rem; }
.chip:disabled { opacity: 0.45; cursor: not-allowed; }
.chip i { font-style: normal; color: var(--color-neutral-600); }
/* The quick-amount row on the bill: the exact figure as the offered chip,
   the common notes beside it. One tap covers the common case. */
.quick { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.quick .chip { font-variant-numeric: tabular-nums; }
/* No colour of its own. This used to set `color: var(--color-accent)` and,
   at 0-3-0, it outranked the `.chip.sel, .chip.on` fill rule further down —
   so the chosen amount was magenta text on a magenta fill and the bill's
   headline chip rendered blank. The filled treatment is the system's; this
   only adds the tabular figures. */
.quick .chip.on { font-weight: 700; }

.filters { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-bottom: var(--sp-4); }
/* The reservation's day and time read as one answer to "when", on one line:
   a row of their own for two day chips pushed the sheet's Save off a laptop. */
.when { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); }
.when input { flex: 1 1 9rem; width: auto; }
/* The picked time in the app's own "9:00 م", beside a box the browser draws
   in its own language (sheets.js timeEcho). */
.when .time-echo { margin: 0; white-space: nowrap; font-variant-numeric: tabular-nums; }
/* Two short fields side by side — the waitlist's phone and head count — so
   the second costs the sheet no height of its own. */
.pair { display: grid; grid-template-columns: minmax(0, 1fr) 9rem; gap: 0 var(--sp-4); }
@media (max-width: 620px) { .pair { grid-template-columns: minmax(0, 1fr) 7rem; } }
.actions { display: flex; gap: var(--sp-4); margin-top: var(--sp-5); }
.actions .btn { flex: 1; justify-content: center; }
.actions .btn-ghost { flex: 0 0 auto; }
/* A sheet's closing actions stay on screen while its fields scroll — the
   picker's footer, given to every sheet. On a phone the edit-room Save sat two
   screens down and the bill's Confirm below the visible viewport. Only the
   LAST row: the session sheet has an "add items" row mid-sheet, and pinning
   both stacked them over each other. The rule and the extra padding stand in
   for the margin the row had, so a sheet that fits is the height it was. */
.sheet > .actions:last-child {
  position: sticky; bottom: calc(-1 * var(--sheet-pb)); z-index: 1;
  background: var(--color-bg); border-top: 1px solid var(--rule);
  margin-top: var(--sp-3);
  padding-block: var(--sp-3) calc(var(--sheet-pb) + env(safe-area-inset-bottom));
  margin-bottom: calc(-1 * var(--sheet-pb) - env(safe-area-inset-bottom));
}
body.dense .sheet > .actions:last-child { margin-top: var(--sp-2); padding-top: var(--sp-2); }
/* A footer's buttons give way inside the sheet rather than past it.
   Tracked English caps run long — the bill's "COLLECT EGP 65 · CASH", the
   picker's "ADD TO ORDERS", "CLOSE SHORT · EGP 55" — and held on one line
   they pushed CANCEL off the start edge of a 360px sheet and the picker
   sideways. A label wraps inside its own button instead, only when the row
   is out of room, and never below its longest word. */
@media (max-width: 620px) {
  .sheet > .actions:last-child .btn,
  .picker-total .btn { flex-shrink: 1; min-width: min-content; white-space: normal; text-align: center; }
}

/* The running session, and the bill. */
.timer-big {
  font-size: var(--fs-10xl); font-weight: 700; font-variant-numeric: tabular-nums;
  color: var(--color-accent); letter-spacing: 0.08em; margin: 2px 0 var(--sp-5);
}
.price-big { font-size: var(--fs-7xl); font-weight: 500; margin-bottom: var(--sp-4); }
.price-big span { font-size: var(--fs-base); font-weight: 400; color: var(--color-neutral-500); }

.lines { display: flex; flex-direction: column; }
.line-row {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: var(--sp-4); padding: var(--sp-3) 0; font-size: var(--fs-lg);
}
/* Baseline, not centre: a flex box with nothing baseline-aligned takes its
   baseline from the picture's bottom edge, which sat every item's price 7px
   below its name while the room-time row above lined up. The picture is
   centred on its own and the name sets the row's baseline. */
.line-row .with-art { display: inline-flex; align-items: baseline; gap: var(--sp-3); min-width: 0; }
.line-row .with-art > .art { align-self: center; }
.line-row > span:first-child { min-width: 0; }
.line-row i { font-style: normal; font-size: var(--fs-sm); color: var(--color-neutral-600); }
/* The label wraps; the amount never does — the same rule the room card's
   money keeps. */
.line-row b { font-weight: 500; font-variant-numeric: tabular-nums; white-space: nowrap; flex: 0 0 auto; }
.line-row .right { display: flex; align-items: center; gap: var(--sp-4); }

.total-row {
  display: flex; justify-content: space-between; align-items: baseline;
  padding: var(--sp-4) 0 2px; font-size: var(--fs-2xl); font-weight: 500;
}
.total-row b {
  font-size: 2rem; font-weight: 700; font-variant-numeric: tabular-nums;
  color: var(--color-accent); white-space: nowrap;
}
/* What a part-paid customer still owes, and the change a cash note is owed
   back: the lines a cashier reads out after the total, a step below it. The
   owed label is the warning; the figure stays money-white. */
.total-row.owed, .total-row.change { padding-top: var(--sp-3); }
.total-row.owed b, .total-row.change b { font-size: 1.65rem; }
.total-row.owed > span { color: var(--color-accent); }
/* The drawer count's answer. Said in words ("short 5"), and a real difference
   takes the owed line's warning; "no gap" stays plain. */
.total-row.variance { padding-top: var(--sp-4); }
.total-row.variance b { font-size: 1.65rem; }
.total-row.variance.off > span { color: var(--color-accent); }

.x {
  background: none; border: 0; padding: 0 var(--sp-2); font: inherit; font-size: var(--fs-3xl);
  line-height: 1; color: var(--color-neutral-600);
}
/* A thumb's target at every size, not only under pointer:coarse — the same
   touchscreen counters the .btn rule is written for. Borrowed padding given
   back with a negative margin, so no row grows; the width centres the glyph,
   which also moves it off the price the finger is usually aiming at. */
.x {
  min-width: 2.9rem; padding-block: 1.05rem; margin-block: -1.05rem;
  display: inline-grid; place-items: center;
}
@media (hover: hover) { .x:hover { color: var(--color-accent); } }

.hint { margin: 0 0 var(--sp-3); font-size: var(--fs-md); color: var(--color-neutral-500); }
/* Under a field the hint needs air: Tajawal's tall ascenders touched the
   input's bottom border, while the same hint under a chip row sat clear. */
input + .hint, textarea + .hint { margin-top: var(--sp-2); }
/* The customer's booking read back before they book: the day and hours stay
   quiet, the estimate is money and so white — even on the line that turns
   to the accent to say a passed time has rolled over to tomorrow. */
.cb-slot { margin-top: var(--sp-3); }
.cb-slot b { color: var(--color-text); font-weight: 500; }
.hint.warn { color: var(--color-accent); }
.hint.paid { color: var(--color-accent); font-weight: 500; }
.micon { vertical-align: -2px; opacity: 0.85; }
.notice {
  margin: 0 0 var(--sp-4); padding: var(--sp-3) 0; font-size: var(--fs-base);
  color: var(--color-neutral-400);
  border-top: 1px solid var(--color-divider);
  border-bottom: 1px solid var(--color-divider);
}
.notice.warnbox { color: var(--color-accent); }

/* A required field left empty: the sentence sits right under the field it is
   about, and the field keeps the accent outline after the caret leaves it —
   the thing to do next, which is the accent's one job. */
.field-err { margin: var(--sp-2) 0 0; }
.filters + .field-err { margin-top: calc(-1 * var(--sp-2)); }
input[aria-invalid="true"] { border-color: var(--color-accent); }

.payrow { display: flex; flex-direction: column; gap: var(--sp-4); margin: var(--sp-4) 0; }
.methods { display: flex; flex-wrap: wrap; gap: var(--sp-3); }

/* The bill's discount. Folded to one quiet word and its limit until it is
   wanted — the exception at a counter should not cost every bill a row of
   boxes — then the amount, an optional reason and Apply on one line. */
.disc-open { display: flex; align-items: baseline; gap: var(--sp-4); padding-top: var(--sp-3); }
.disc-open .hint { margin: 0; }
.disc-box label { margin-top: var(--sp-3); }
.disc-row { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: stretch; }
.disc-row #disc { flex: 0 1 8rem; min-width: 6rem; font-variant-numeric: tabular-nums; }
.disc-row #disc-why { flex: 1 1 10rem; min-width: 8rem; font-size: var(--fs-lg); }
.disc-row .btn { flex: 0 0 auto; }
/* The applied line's ×, beside its words rather than its amount, so the
   amount keeps the bill's one column of money. Its thumb-sized box is drawn
   around the glyph, never into the row's height. */
.disc-line .x { vertical-align: middle; margin-inline-start: var(--sp-1); }

/* The item picker. The one place pictures survive the redesign: the handoff
   covers the nine full screens, and a counter picking drinks by name alone is
   slower than one picking them by sight. */
.items { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 8rem), 1fr)); gap: var(--sp-4); }
.item {
  display: flex; flex-direction: column; gap: var(--sp-2); text-align: center;
  background: var(--color-bg);
  border: var(--hair-quiet); border-radius: var(--radius-lg);
  padding: var(--sp-4) var(--sp-3) var(--sp-3);
  transition: border-color 0.12s;
}
@media (hover: hover) { .item:hover { border-color: var(--color-text); } }
.item.picked { border-color: var(--color-accent); }
.item .nm { font-size: var(--fs-base); font-weight: 500; }
.item .pr { font-size: var(--fs-md); color: var(--color-neutral-500); font-variant-numeric: tabular-nums; }
/* A picked card says "chosen" once, with its outline. The line's sum is
   money, so it is white and bold; the badge is a count, so it is grey. */
.item .pr.sum { color: var(--color-text); font-weight: 700; }
.art-wrap { position: relative; display: grid; place-items: center; margin-bottom: 2px; }
.art { display: block; }
.qty-badge {
  position: absolute; inset-block-start: -4px; inset-inline-end: -2px;
  font-size: var(--fs-sm); font-weight: 700; color: var(--color-neutral-400);
  font-variant-numeric: tabular-nums;
}
.add {
  margin-top: auto; width: 100%; font: inherit; font-size: var(--fs-md);
  color: var(--color-accent); background: none;
  border: 1px solid var(--color-divider); border-radius: var(--radius-sm);
  padding: var(--sp-3) var(--sp-2); min-height: 2.6rem;
  /* Two words on one line, always — a button that breaks into two rows on
     the dense counter reads as a broken card, not a button. */
  white-space: nowrap;
}
@media (hover: hover) { .add:hover { border-color: var(--color-accent); } }
.stepper { display: flex; align-items: center; gap: var(--sp-3); margin-top: auto; }
.stepper button {
  width: 2rem; height: 2rem; flex: 0 0 auto; font: inherit; font-size: var(--fs-3xl);
  line-height: 1; color: var(--color-text); background: none;
  border: 1px solid var(--color-divider); border-radius: var(--radius-sm);
}
@media (hover: hover) { .stepper button:hover { border-color: var(--color-text); } }
.stepper .qty {
  flex: 1; text-align: center; font-size: var(--fs-2xl); font-weight: 500;
  font-variant-numeric: tabular-nums;
}

.picker-total {
  position: sticky; bottom: calc(-1 * var(--sheet-pb));
  margin: var(--sp-5) calc(-1 * var(--sheet-px)) calc(-1 * var(--sheet-pb));
  padding: var(--sp-4) var(--sheet-px) calc(var(--sp-5) + env(safe-area-inset-bottom));
  background: var(--color-bg);
  display: flex; align-items: center; gap: var(--sp-4);
}
.picker-total::before {
  content: ""; position: absolute; inset-inline: 0; top: 0; height: 1px;
  background: var(--rule);
}
.picker-total .sum { flex: 1; }
.picker-total .sum span { display: block; font-size: var(--fs-sm); color: var(--color-neutral-500); }
.picker-total .sum b { font-size: var(--fs-5xl); font-weight: 700; font-variant-numeric: tabular-nums; }
/* A counter sale's footer asks how it was paid: the bill's four method chips
   as one line of their own above the sum, the chosen one filled like every
   chosen chip. Cash is chosen before anyone looks, so the sale that is cash —
   most of them — is still sum, Sell, gone. */
.picker-total.with-pay { flex-wrap: wrap; row-gap: var(--sp-3); }
.pay-by { flex: 1 0 100%; display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-3); }
.pay-by-l { font-size: var(--fs-sm); color: var(--color-neutral-500); text-transform: uppercase; margin-inline-end: var(--sp-2); }
.pay-by .chip { padding-inline: var(--sp-3); }
/* On a phone the four chips share the line evenly rather than wrapping one
   onto a second row — the footer is sticky, and every row it grows is a row
   of drinks it covers. The label gives its place to them; the group keeps
   it as its name. */
@media (max-width: 620px) {
  .pay-by { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--sp-2); }
  .pay-by-l { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  .pay-by .chip { padding-inline: var(--sp-1); font-size: var(--fs-sm); justify-content: center; }
  .pay-by .chip .micon { display: none; }
}

.artpick { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.artopt {
  background: var(--color-bg); border: var(--hair-quiet);
  border-radius: var(--radius-sm); padding: var(--sp-2); display: grid; place-items: center;
}
.artopt.sel { border-color: var(--color-accent); }
.artopt.wide { padding: 0 var(--sp-4); min-height: 2.9rem; font-size: var(--fs-base); color: var(--color-neutral-500); }
.artopt.wide.sel { color: var(--color-accent); }

/* --------------------------------------------------------------- install -- */

.install {
  display: flex; align-items: baseline; gap: var(--sp-4); flex-wrap: wrap;
  padding: var(--sp-3) 0; border-bottom: 1px solid var(--color-divider);
}
.install b { font-size: var(--fs-lg); font-weight: 500; }
.install p { margin: 0; font-size: var(--fs-md); color: var(--color-neutral-500); }
.install .install-actions { margin-inline-start: auto; display: flex; gap: var(--sp-5); }
/* On a phone it was 130px above the money: the title, the steps as two lines
   of sentence, then LATER alone on a row. Now it is two short lines — the
   title with its buttons, and the how under them in the quiet small size —
   and the steps say the same two taps in fewer words, each button drawn as
   the phone draws it (views-owner.js). */
@media (max-width: 620px) {
  .install {
    display: grid; grid-template-columns: minmax(0, 1fr) auto;
    align-items: baseline; gap: var(--sp-1) var(--sp-4);
  }
  .install b { grid-area: 1 / 1; font-size: var(--fs-base); line-height: var(--lh-ui); }
  .install .install-actions { grid-area: 1 / 2; gap: var(--sp-5); }
  .install p { grid-area: 2 / 1 / 3 / 3; font-size: var(--fs-sm); line-height: var(--lh-ui); }
}
.install .glyph { vertical-align: -2px; opacity: 1; }

/* ---------------------------------------------------------------- footer -- */

/* ----------------------------------------------------------- phone frame -- */
/* The owner app behind glass, on any screen. The iframe inside renders the
   TRUE phone layout — its media queries answer to the frame's own 393px
   viewport, not the desktop's. */
.phone-stage {
  align-items: center; justify-content: center; gap: var(--sp-3);
  overflow: hidden;
}
/* The frame is a simulator, not a resizable box: the screen inside is ALWAYS
   a true 393×852 phone viewport, and the whole device scales visually to fit
   (or to fill — up to 1.15× on a roomy desktop). Ratios and text layout
   therefore never change with the window; only the size does. The fit box is
   sized by script to the scaled device so the page lays out around it. */
.phone-fit { direction: ltr; flex: 0 0 auto; }
.phone {
  /* 393×852 of screen + 10px bezel + 1px border each side. */
  width: 415px; height: 874px;
  transform-origin: top left;
  background: var(--color-bg);
  /* The device frame is a white outline on black, exactly as the reference
     draws it — and no shadow: this system has none. */
  border: var(--hair);
  border-radius: 44px; padding: 10px;
  position: relative;
}
/* The Dynamic Island — sized in percentages so it shrinks with the frame,
   and physical left/transform on purpose: it is glass, not text, and must
   not flip with the page direction. */
.phone-notch {
  position: absolute; top: 2.1%; left: 50%; transform: translate(-50%, 0);
  width: 26%; height: 3.1%; border-radius: 999px;
  background: var(--color-bg);
  border: 1px solid var(--color-ash);
  z-index: 2; pointer-events: none;
}
.phone-screen {
  width: 100%; height: 100%; border-radius: 35px; overflow: hidden;
  background: var(--color-bg);
}
.phone-screen iframe { width: 100%; height: 100%; border: 0; display: block; }
.phone-cap {
  font-size: var(--fs-sm); color: var(--color-neutral-500);
  text-align: center; max-width: 26rem; margin: 0;
}
/* On a landscape window the caption stands beside the device, not under it.
   Stacked, it took its height out of the phone's: at the 1280×800 counter
   reference the device scaled to 0.69 and its type landed at 7–8px, with
   half a metre of black on either side doing nothing. Beside it, the height
   is the phone's alone (applyPhoneScale reads which way the stage runs). */
@media (min-aspect-ratio: 4/3) {
  main.phone-stage { flex-direction: row; gap: var(--sp-7); }
  .phone-cap { max-width: 16rem; text-align: start; }
}
/* Inside the glass, the demo's own furniture disappears — and the app's bar
   drops below the island, the way a real phone reserves status-bar space. */
body.embed footer { display: none; }
body.embed .top { padding-top: 2.2rem; }
/* The owner's and the customer's phone apps end in their own tab bar, which
   is where a thumb is and what makes them feel installed. The demo's footer
   sat UNDER it — the bar lifted 46px off the edge, two strips stacked, and the
   notch's inset paid twice. On those surfaces, and in the installed app, the
   bar is the bottom of the screen, and the footer's line closes the page's
   content instead (.demo-foot, app.js demoFoot) — Reset demo reachable on
   every surface, and never two of it on one screen: the pair of rules that
   hides the one shows the other. */
.demo-foot { display: none; }
@media (max-width: 620px) {
  body:has(#app .tabs.bottom-nav) footer { display: none; }
  body:not(.touring):has(#app .tabs.bottom-nav) .demo-foot { display: flex; }
}
@media (display-mode: standalone) {
  footer { display: none; }
  body:not(.touring) .demo-foot { display: flex; }
}
/* The footer's own line and look, as the last thing on the page: pushed to
   the foot of a short screen, one gap under a long one. Without the footer's
   rule — most screens end in a list whose last row already draws one, and two
   hairlines a gap apart read as a stutter. */
.demo-foot {
  flex: 0 0 auto; margin-top: auto;
  gap: var(--sp-5); justify-content: center; align-items: center;
  padding-top: var(--sp-4);
  font-size: var(--fs-xs); color: var(--color-neutral-600);
}
.demo-foot button {
  color: var(--color-neutral-600); background: none; border: 0; padding: var(--sp-2) var(--sp-4);
  font: inherit; font-size: var(--fs-xs); text-decoration: underline;
}
/* Under a thumb it is a thumb's height, like the footer's own (below). */
@media (pointer: coarse) { .demo-foot button { min-height: max(2.75rem, 44px); } }
@media (hover: hover) { .demo-foot button:hover { color: var(--color-neutral-400); } }

footer {
  flex: 0 0 auto;
  display: flex; gap: var(--sp-5); justify-content: center; align-items: center;
  padding: var(--sp-3) var(--sp-4) calc(var(--sp-3) + env(safe-area-inset-bottom));
  font-size: var(--fs-xs); color: var(--color-neutral-600);
  border-top: 1px solid var(--color-ash);
}
footer a, footer button {
  color: var(--color-neutral-600); background: none; border: 0;
  font: inherit; font-size: var(--fs-xs); text-decoration: underline;
}
@media (hover: hover) { footer a:hover, footer button:hover { color: var(--color-neutral-400); } }

/* ------------------------------------------------------------ walkthrough -- */

.tourbar {
  position: fixed; inset-inline: 0; bottom: 0; z-index: 20;
  display: flex; align-items: center; gap: var(--sp-5);
  padding: var(--sp-4) var(--pad-x) calc(var(--sp-4) + env(safe-area-inset-bottom));
  background: var(--color-carbon);
  border-top: 1px solid var(--color-accent);
  max-width: 1680px; margin: 0 auto;
}
.tourbar .tourtext { flex: 1; min-width: 0; }
/* 58% rather than 46%: on the bar's carbon the quieter grey was 4.27:1 at
   twelve pixels, under what small text needs. */
.tourbar .step { display: block; font-size: var(--fs-2xs); color: var(--color-neutral-500); }
.tourbar p { margin: 0; font-size: var(--fs-lg); line-height: 1.45; }
.tourbar .tourbtns { display: flex; gap: var(--sp-4); flex-shrink: 0; }

/* The bar takes its room out of the shell, so nothing has to scroll to clear
   it — --tour-h is its measured height, set in app.js on every render. */
body.touring { padding-bottom: var(--tour-h, 0px); }
/* With the bar up there is genuinely no row to spare: the walkthrough alone
   gives up the section labels — the cards stay sorted in-use first. */
body.touring .sect-head { display: none; }
/* And the bill's folded discount word, for the same reason: the narration
   settles a split payment, never a discount, and on the lived-in café (whose
   limit is 20) that one line was what pushed the bill sheet into scrolling
   under the bar. Outside the walkthrough it is always there. */
body.touring .disc-open { display: none; }
/* Two more lines the narration says aloud, and on the lived-in café the bill
   and the picker scrolled under the bar without them (a 1280×800 counter by
   7px, a 393px phone by 28px): the split hint, which the next step speaks
   ("one bill, two methods"), and the shelf's "5 left" on the juice, which the
   picker's narration never mentions. Outside the walkthrough both are there. */
body.touring .split-hint,
body.touring .item .stock-note { display: none; }
body.touring footer { display: none; }
body.touring .install { display: none; }
body.touring main { padding-bottom: var(--sp-3); gap: var(--sp-4); }
body.touring .sheet {
  bottom: var(--tour-h, 0px);
  max-height: calc(100dvh - var(--tour-h, 0px) - 12px);
}
/* On a phone the two buttons took over half the bar's row and the narration
   wrapped to seven lines — a quarter of the screen, sixty pixels above the
   table it was describing. One row still, but the buttons stack in a column
   of their own — the exit a quiet word over Next, its touch height kept —
   and the narration steps down to the body size, so the words get the width. */
@media (max-width: 620px) {
  .tourbar { gap: var(--sp-3); }
  .tourbar p { font-size: var(--fs-base); }
  .tourbar .tourbtns { flex-direction: column; align-items: stretch; gap: 0; }
  .tourbar [data-action="tour-exit"] {
    border-color: transparent; padding-inline: var(--sp-1); color: var(--color-neutral-400);
  }
  .tourbar [data-action="tour-next"] { padding-inline: var(--sp-4); }
  /* Headroom, not a fit to the pixel. On the lived-in café in English the
     picker left 7px under the bar and the bill 1px, and a browser that sets
     text a hair taller — CI's does — scrolled them both. The walkthrough
     alone draws the bill's rows and the picker's grid a step tighter, which
     buys back some 50px and 30px; outside it every row keeps its air. */
  body.touring .sheet .line-row { padding-block: var(--sp-2); }
  body.touring .sheet .items { gap: var(--sp-2); }
  body.touring .sheet .item { padding-top: var(--sp-2); }
}

.spot {
  outline: 1px solid var(--color-accent); outline-offset: 5px;
  border-radius: var(--radius-md);
  animation: spot-pulse 1.8s ease-in-out infinite;
}
@keyframes spot-pulse {
  0%, 100% { outline-color: var(--color-accent); }
  50% { outline-color: color-mix(in srgb, var(--color-accent) 35%, transparent); }
}

/* ----------------------------------------------------------------- toasts -- */

.toast {
  position: fixed; inset-inline: var(--sp-6); bottom: 4rem; z-index: 30;
  max-width: 34rem; margin: 0 auto;
  background: var(--color-carbon);
  border: var(--hair); border-radius: var(--radius-lg);
  padding: var(--sp-4) var(--sp-5); font-size: var(--fs-base); color: var(--color-text);
  animation: rise 0.18s cubic-bezier(0.22, 0.8, 0.3, 1);
}
/* A confirmation is the filled magenta — the one place the system shouts, and
   the same fill as the primary button it usually follows. */
.toast.ok { background: var(--color-accent); border-color: var(--color-accent); color: #fff; }
/* The undo on a removal toast: the one thing on it to press. */
.toast .link { margin-inline-start: var(--sp-5); font-size: var(--fs-base); }
/* A toast that stays until it is put away carries its × at the end. */
.toast.sticky { display: flex; align-items: center; gap: var(--sp-3); }
.toast.sticky .toast-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.toast .x { color: inherit; flex: 0 0 auto; }
/* With a sheet up, a toast drops in from the top. Below the dialog width the
   sheet is a drawer that owns the bottom of the screen, and a toast there sat
   on its total — the very number the toast was about. On a wide screen a tall
   dialog now ends in its pinned actions, and a toast at the bottom covered
   the Save it was reporting on. */
.sheet ~ .toast { top: var(--sp-5); bottom: auto; }

.empty { font-size: var(--fs-lg); color: var(--color-neutral-500); }
.empty.sm { font-size: var(--fs-base); margin: var(--sp-1) 0 0; }

/* An empty list says what the screen is for and offers the one next step,
   instead of a bare sentence (modernisation pass). */
.empty-box {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: var(--sp-3); padding: var(--sp-6) 0;
}
.empty-box .empty { color: var(--color-neutral-400); }
.empty-box .helper { margin: 0; }
.empty-box .btn { margin-top: var(--sp-2); }

/* ------------------------------------------------------------ touch input -- */

/* A finger is not a mouse pointer. On any touch device — the owner's phone, and
   a counter machine with a touchscreen, which several of these cafés have — the
   pressable things grow to a comfortable target. Sizes are still in rem, so
   this stacks with the fluid scale rather than fighting it. */
@media (pointer: coarse) {
  .btn { min-height: 3rem; padding-inline: var(--sp-5); }
  .btn-lg { min-height: 3.2rem; }
  .chip { min-height: 3rem; }
  .add { min-height: 3rem; }
  /* Text buttons already borrow a thumb's height at every size (.link). A
     row's action words also borrow width, out of the gap beside them: "Hide"
     and "Cancel" were 30px wide targets on a phone, and the row has the
     slack. Not the header's words — see .link for why. */
  .tbl-row .acts .link, .row-actions .link {
    padding-inline: var(--sp-3); margin-inline: calc(-1 * var(--sp-3));
  }
  /* The language switch is a single glyph, "ع", 8px wide. It borrows the
     header's gap on both sides — exactly the gap, so it reaches its
     neighbours without covering them, and the row does not grow: a real
     2.75rem box pushed SIGN OUT onto a line of its own below 360px. It is
     never the trailing word, so the inline margin cannot scroll the page. */
  [data-action="lang"] { padding-inline: var(--sp-5); margin-inline: calc(-1 * var(--sp-5)); }
  /* The day picker and the demo's own Reset sit under a thumb as well. The
     footer gives up its padding for the taller button, so the shell above it
     loses no height. */
  .day-pick { min-height: 3rem; }
  footer { padding-block: 0 env(safe-area-inset-bottom); }
  footer button { min-height: max(2.75rem, 44px); padding-inline: var(--sp-4); }
  /* The surface switch draws its own outline, so its target is its real box
     rather than borrowed padding that would inflate the outline with it. */
  .roles button { min-height: 2.75rem; }
  .tabs button { padding-block: var(--sp-5) var(--sp-3); }
  .mini-tabs button { padding-block: var(--sp-4); }
  /* The two 45px buttons and the count came to 118px in a 92px card and the
     plus hung over the next drink. They share the card's width instead,
     keeping the height a thumb needs. */
  .stepper { display: grid; grid-template-columns: 1fr auto 1fr; gap: 2px; }
  .stepper button { width: 100%; min-width: 0; height: 2.8rem; }
  input { min-height: 3rem; }
}

/* ------------------------------------------------------------- densities -- */

/* Short on room — a small laptop, or the walkthrough bar taking its share.
   Nothing is removed and nothing shrinks below legibility: the spacing gives
   way first, and only the display-sized numbers step down. Because these are
   the same tokens the rest of the file reads, this is the whole of it. */
body.dense {
  --lh-body: 1.55;

  --sp-4: 0.71rem;
  --sp-5: 0.93rem;
  --sp-6: 1.14rem;
  --sp-7: 1.57rem;
  --sp-8: 2.29rem;

  --fs-11xl: 2.6rem;
  --fs-10xl: 2.14rem;
  --fs-9xl: 1.86rem;
  --fs-8xl: 1.57rem;
  --fs-7xl: 1.43rem;
  --fs-6xl: 1.29rem;
  --fs-5xl: 1.21rem;

  --bar-h: 3.2rem;
  --room-h: 7.4rem;
  --chart-h: 6.9rem;
}
body.dense .sheet { --sheet-pb: 0.86rem; --sheet-px: 1.5rem; padding-top: 1.1rem; }
/* On a short screen a form's fields close up before its type does: the space
   above a label is the first thing that can go, and the last thing anybody
   notices going. */
body.dense label { margin-top: var(--sp-3); }
body.dense .sheet h3 { margin-top: var(--sp-4); }
body.dense .payrow { gap: var(--sp-3); margin-block: var(--sp-3); }
body.dense .actions { margin-top: var(--sp-4); }
/* Short on height, the grouping keeps its labels — a laptop browser lives in
   dense mode, and the grouping IS the screen's shape. The room the labels cost
   comes back out of the cards and the gaps instead. */
body.dense .room { padding: var(--sp-3) var(--sp-4); }
body.dense .grid { gap: var(--sp-3); }
body.dense .sect-head { margin-top: 0; font-size: var(--fs-md); line-height: 1.2; }
body.dense .room .timer { font-size: 1.6rem; }
/* The management rows carry a meta line now; short screens take it out of the
   row's padding rather than out of the line. */
body.dense .tbl-row { padding: var(--sp-3) 0; }
/* And the picker's cards pack a column tighter, so the sheet clears the
   walkthrough bar without scrolling. */
body.dense .items { grid-template-columns: repeat(auto-fill, minmax(min(100%, 5.7rem), 1fr)); }
body.dense .item { padding: var(--sp-3) var(--sp-2) var(--sp-2); }
/* A drink's name and its price are one short line each. Body leading is for
   sentences; on a short screen these two give it back, which is what keeps
   the picker off a scrollbar with the walkthrough bar up. */
body.dense .item .nm, body.dense .item .pr { line-height: 1.25; }
body.dense .add { min-height: 2.2rem; font-size: var(--fs-sm); }

/* A phone. The counter is a wide screen by rule (CLAUDE.md section 6), so this
   is the owner's surface and the salesman's demo. */
@media (max-width: 620px) {
  /* THE HEADER IS TWO ROWS ON A PHONE, AND THE BRAND IS NOT ONE OF THEM.
     The brand used to stack its own two lines here — the café's name over a
     small caption — and when the mark took the caption's slot that rule
     started stacking a 34px image ON TOP OF a display-sized name inside a bar
     with a FIXED height. Everything after it went wrong at once: the mark
     clipped against the top of the screen, the name overflowed onto the tab
     rule below it and truncated to a fragment, and the controls floated at
     half height between the two. It looked like a rendering bug and it was a
     stale rule.

     The constraint the old rule was answering is real, and the comment below
     still states it: on a 320px phone the control cluster alone is the whole
     width, so the mark, the café's name and the controls cannot share a line.
     So the BAR wraps rather than the brand — identity on the first row,
     controls on the second, each on a line it actually fits. The mark sits
     BESIDE the name, which is the one arrangement that reads as a wordmark.

     The embedded copy (the phone frame on the landing page, and every -m
     screenshot) carries the connection dot and nothing else, so it keeps its
     single fixed-height row: wrapping there would spend a line of an already
     short screen on one word. */
  .brand { flex-direction: row; align-items: center; gap: var(--sp-4); }
  .brand strong { font-size: var(--fs-xl); }
  .brand .demo { display: none; }
  body:not(.embed) .bar {
    height: auto; flex-wrap: wrap;
    padding-block: var(--sp-3); row-gap: var(--sp-2);
  }
  body:not(.embed) .brand { flex: 1 1 100%; }
  body:not(.embed) .controls { flex: 1 1 100%; margin-inline-start: 0; }
  /* The header cluster is the one row with no slack: on a 320px phone the
     connection dot, the two text buttons and the three-way role switch add up
     to the whole width, so the gaps and the type give way before anything is
     dropped. */
  .bar { gap: var(--sp-3); }
  /* Settings on one line, the surface switch across the full width under
     them: the switch is a place to go, not one more setting, and on a phone
     it gets a row of its own rather than being squeezed off the edge. */
  body:not(.embed) .controls { flex-wrap: wrap; row-gap: var(--sp-3); }
  .controls { gap: var(--sp-5); font-size: var(--fs-sm); }
  .controls .sep { display: none; }
  .roles { flex: 1 1 100%; }
  .roles button { flex: 1 1 0; font-size: var(--fs-sm); padding-inline: var(--sp-2); }
  /* On an actual phone the frame is pointless — this screen already IS the
     phone rendering — and the header row has no slack for a fourth word. */
  .roles .role-phone { display: none; }
  .tabs { gap: var(--sp-6); }
  footer { gap: var(--sp-5); }

  /* Two columns, not one. A single column of cards turned the grid into a
     scroll, and the whole point of it is seeing the café at a glance. */
  .grid { grid-template-columns: repeat(auto-fill, minmax(min(46%, 10.5rem), 1fr)); gap: var(--sp-3); }
  .room { min-height: 8rem; padding: var(--sp-4) var(--sp-4); }
  /* The type keeps its place on a phone, on a line of its own under the
     name. It was hidden to uncrowd the head row — and with the name already
     "Room 1", nothing on the booking app's card said PS5 or PS4 any more,
     which is what a customer books by. The zero-height break forces the
     badge down whatever the name's length, so the state word keeps the
     corner (above) on every card; the name's zero basis keeps a long name
     from pushing the state word down a line instead. */
  .room .room-head { flex-wrap: wrap; row-gap: 0; }
  .room .room-head::after { content: ""; order: 2; flex-basis: 100%; }
  .room .name { flex: 1 1 0; }
  .room .state { order: 1; }
  .room .type { order: 3; margin-top: var(--sp-1); }
  /* English caps in the monospace run far wider than the Tajawal the card
     was sized for: "BILLIARD" needed 77px beside "Available" and had 66, so
     it broke mid-word. A step down and a tighter gap fit the words a café
     actually uses; a longer one still breaks inside its own box (above). */
  html[lang="en"] .room .name { font-size: var(--fs-md); }
  html[lang="en"] .room-head { gap: var(--sp-2); }
  .room .timer { font-size: 1.5rem; }
  .room .price { font-size: 1.3rem; }

  /* The owner's phone gets a bottom tab bar — the PWA launch surface. The nav
     is the same element the desktop shows as top tabs; here it drops to the
     bottom of the shell, items centred, the accent mark on top. */
  .tabs.bottom-nav {
    order: 10;
    border-top: 1px solid var(--color-ash);
    padding: var(--sp-2) var(--sp-2) calc(var(--sp-3) + env(safe-area-inset-bottom));
    gap: 0;
    /* Nine owner tabs no longer fit a phone — the bar scrolls sideways
       instead of clipping labels, and each tab keeps a readable width. */
    overflow-x: auto;
    scrollbar-width: none;
  }
  .tabs.bottom-nav::-webkit-scrollbar { display: none; }
  .tabs.bottom-nav button {
    flex: 1 0 auto; min-width: 3.9rem; align-items: center; gap: 5px;
    padding: var(--sp-2) var(--sp-2); font-size: var(--fs-xs); min-height: 3.05rem;
    white-space: nowrap;
  }
  .tabs.bottom-nav .bar-mark { order: -1; width: 1.06rem; }
  .tabs.bottom-nav + .hr { display: none; }

  .stats { gap: var(--sp-6); }
  .figures { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--sp-4) var(--sp-6); }
  /* A sheet's figures are three short amounts, not the dashboard's four —
     three columns on one line, each as wide as it needs. Wrapping, they left
     "EGP 30 / Price" alone on a second row; a step down in size fits them,
     and a long label ("Room time · minimum") folds under its own amount
     without moving it (below). The columns are spread by space-between, so
     the gap is only the least they may close to: at a full gap the English
     "EGP 30/hour" ran off a 360px sheet. */
  .sheet .figures {
    display: grid; grid-template-columns: repeat(3, auto);
    justify-content: space-between; gap: var(--sp-4) var(--sp-3);
  }
  .sheet .figure b { font-size: 1.35rem; }
  /* An amount is one token already (money() joins it with a no-break
     space); a rate's unit may drop under its price on the narrowest phone
     rather than push the sheet sideways. */
  .sheet .figure b .unit { display: inline-block; }
  /* A label that wrapped pushed its value up off the line its neighbours'
     values share (the figures are bottom-aligned, above): "Sessions · 2
     active" beside "Occupancy" put 2 and 29% eighteen pixels apart. On a
     phone's grid each figure takes two rows of it — the values share the
     first, bottom-aligned, the labels the second — so a label may wrap as
     far as it needs and no value moves. */
  .figure { min-width: 0; }
  .figures:not(.inline) > .figure {
    display: grid; grid-row: span 2; grid-template-rows: subgrid; row-gap: 0;
    justify-content: start;
  }
  .figures:not(.inline) > .figure > :first-child { align-self: end; }
  /* The time column holds "12:00 ص" — the widest this clock prints — on ONE
     line: a step down in size and a column sized for a two-digit hour beside
     the wide meridiem, so neither drops under the number and the date under
     that. Measured, not guessed: 96px needed, 6.4rem gives 103px. */
  .row { grid-template-columns: 6.4rem 1fr; gap: var(--sp-4); }
  .row-when b { font-size: 1.2rem; white-space: nowrap; }
  .row-actions { grid-column: 1 / -1; }
  .row.flat { grid-template-columns: 1fr auto; }
  /* The reports' hero and its one-line summary sit together on a phone. */
  main.own-reports .hero-row { gap: var(--sp-3); }

  /* The wide tables collapse into three lines: the name, then everything that
     was a column as one wrapped meta line, then the actions. A column each
     would be eight lines a room. */
  .tbl-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-1) var(--sp-4); }
  .tbl-head { display: none; }
  .tbl-row > b, .tbl-row .with-note { flex: 1 1 100%; }
  .tbl-row .acts { flex: 1 1 100%; justify-content: flex-start; gap: var(--sp-6); margin-top: var(--sp-1); }
  /* The header row is gone, so a report cell carries its own column name: a
     row of six bare numbers ("1 2.0 120 50 170 170") says nothing on a phone,
     and the phone is the owner's surface. Each labelled cell becomes a small
     figure — the name over the value — and the row reads as a stat strip. */
  .tbl-row .cell[data-l] {
    display: inline-flex; flex-direction: column; gap: 1px; min-width: 3.4rem;
  }
  .tbl-row .cell[data-l]::before {
    content: attr(data-l); font-size: var(--fs-3xs); color: var(--color-neutral-600);
    line-height: 1.2; white-space: nowrap;
  }
  .tbl-row .cell.wide[data-l] { flex: 1 1 100%; }
  /* A note is its words: the block takes the row, its two actions under it. */
  .tbl-row.cols-note > .id { flex: 1 1 100%; }

  .sheet { --sheet-px: 1rem; }
  .items { grid-template-columns: repeat(auto-fill, minmax(min(100%, 6.6rem), 1fr)); }
  .toast { inset-inline: var(--sp-4); }
}

/* Someone who has asked for less movement gets none of it. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ------------------------------------------------ the marks, retired ------
 * The 3D render set — the type marks on the room cards, the status marks
 * beside the words, the plastics on the nav tabs and the gamepad beside the
 * café name — is gone with the Kippo re-theme of 31-8-2026.
 *
 * Not because it was bad. Because this system draws everything as a 1px
 * outline on a void and says so twice: imagery is product-internal only, and
 * depth comes from layering and outline contrast, never from a rendered
 * surface. A glossy plastic PS5 tower inside a wireframe card is two design
 * languages arguing on one screen, and the render always wins the argument
 * by volume. The status marks had a second problem: the "free" tick was
 * green, and green is not a colour this system has.
 *
 * What replaced each of them is text, which is what a monospace system wants
 * anyway: the room says "غرفة رقم N" beside its type in a ghost
 * outline badge, status is the word in its status colour, tabs are uppercase
 * words with a magenta underline, and the brand is the wordmark.
 *
 * The one place pictures survive is the item picker (.art / .art3d): a
 * counter picking drinks by name alone is slower than one picking them by
 * sight, that is a speed requirement rather than decoration, and the
 * reference tolerates product-internal imagery. The files themselves stay in
 * icons/ — the customer app and the install guide still use them.
 */


/* The upload is our own ghost button, in the app's language, beside the
   item's current picture — "this one, or a new one". The browser's "Choose
   file / No file chosen" came in the browser's language, English on most
   phones here, inside an Arabic sheet. The real input is inside the label,
   unseen but focusable, so the button's ring is its own. The picked file's
   name follows it, quiet and cut short rather than wrapped. */
.file-pick { position: relative; margin: 0; }
.file-pick:has(input:focus-visible) { outline: 2px solid var(--color-accent-400); outline-offset: 2px; }
.artpick .file-name {
  margin: 0; min-width: 0; flex: 1 1 8rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.artpick { display: flex; align-items: center; gap: var(--sp-4); margin-bottom: var(--sp-4); }
img.art { object-fit: contain; }

/* The re-lit charcoal block that used to sit here is gone: it restated the
   whole charcoal-and-red palette a second time, and the canvas is the void
   now. One palette, at the top of the file, and nowhere else. */
.spark-bar.today .spark-val { color: var(--color-accent); }
@media (hover: hover) { .net:hover { color: var(--color-accent); } }
footer { border-top-color: var(--color-ash); }

/* ------------------------------------------------ streamlined dashboard --
   One bounded column; the hero, the KPI row and the two breakdowns become
   three aligned bands instead of loose type on the ground. */
main.own-dashboard, main.own-undefined { max-width: 72rem; }
/* Two columns, not three: the hero card, then the week beside it taking the
   rest of the band. A third `auto` column used to be reserved for an aside
   the dashboard never renders, and the chart stopped at 30rem — so a third
   of the band sat empty on every counter screen. */
main.own-dashboard .hero-row, main.own-undefined .hero-row {
  display: grid; grid-template-columns: minmax(15rem, 20rem) minmax(0, 1fr);
  gap: var(--sp-8); align-items: center;
  background: var(--color-surface); border: 1px solid var(--color-divider);
  border-radius: var(--radius-lg); padding: var(--sp-5) var(--sp-6);
}
main.own-dashboard .hero, main.own-undefined .hero { width: 100%; }
main.own-dashboard .spark-wrap, main.own-undefined .spark-wrap { max-width: none; }
main.own-dashboard .spark, main.own-undefined .spark { gap: var(--sp-3); }
main.own-dashboard .spark-days, main.own-undefined .spark-days { gap: var(--sp-3); }
main.own-dashboard .figures, main.own-undefined .figures {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--sp-6); margin-top: var(--sp-5);
  background: var(--color-surface); border: 1px solid var(--color-divider);
  border-radius: var(--radius-lg); padding: var(--sp-5) var(--sp-6);
}
main.own-dashboard .duo, main.own-undefined .duo {
  margin-top: var(--sp-5); gap: var(--sp-5);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
}
main.own-dashboard .duo > .col, main.own-undefined .duo > .col {
  background: var(--color-surface); border: 1px solid var(--color-divider);
  border-radius: var(--radius-lg); padding: var(--sp-4) var(--sp-6) var(--sp-5);
}

/* The chrome rows read as words. */
.brand { align-items: center; }
.room { gap: var(--sp-1); }
.room .price, .room .seats, .room .muted { margin-top: 0; }

/* Alignment pass: action words end-aligned so every row's edge matches;
   digit-leading cells kept in the paragraph direction. */
.tbl-row .acts { display: flex; gap: var(--sp-5); justify-content: flex-end; }
.tbl-row .cell, .tbl-head span { unicode-bidi: isolate; text-align: start; }
/* Fixed action-column widths: 'auto' sized per row (each .tbl-row is its own
   grid), which slid the middle column around row to row. */
.cols-rooms { grid-template-columns: 1fr 10rem 14rem; }
.cols-menu { grid-template-columns: 1fr 7.9rem 13rem; }
.cols-staff { grid-template-columns: 1fr 8.6rem 13rem; }
.cols-expense { grid-template-columns: 1fr 8.6rem 12rem; }

.brow .stack { display: inline-flex; flex-direction: column; gap: 2px; }
.brow .stack .meta { font-size: var(--fs-sm); color: var(--color-neutral-500); }

/* The week's numbers ride on top of their bars — a bare figure on one line,
   with a single-line leading and a strip of headroom that holds it. With the
   body's 1.7 leading the tallest bar's label was taller than the strip, and
   the best day, the one bar the chart singles out, lost the top of its
   number. */
.spark { padding-top: 1.3rem; }
.spark-bar { position: relative; }
.spark-bar .spark-val {
  position: absolute; bottom: 100%; inset-inline: 0; margin-bottom: 2px;
  text-align: center; font-style: normal; font-size: var(--fs-3xs);
  line-height: 1.2;
  color: var(--color-neutral-500); font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.spark-bar.today .spark-val { color: var(--color-accent); font-weight: 700; }
/* On a phone the figures take the step their weekdays took (the short names,
   above): they are money, the one thing on the screen that should never be
   the hardest to read, and a bare three-digit figure fits its column at it.
   The strip's headroom still holds the line. */
@media (max-width: 620px) {
  .spark-bar .spark-val { font-size: 0.78rem; }
  /* The columns take the width the names need out of the gaps between them,
     bars and names alike so each stays over the other, and the strip's side
     clip leaves a margin for an end name that still spills. */
  main.own-dashboard .spark, main.own-undefined .spark,
  main.own-dashboard .spark-days, main.own-undefined .spark-days { gap: var(--sp-2); }
  .spark-wrap { overflow-clip-margin: var(--sp-3); }
}

.tbl-row .id { min-width: 0; }
.tbl-row .id .with-note b { white-space: nowrap; }
/* Wide enough for "Take out for maintenance" beside EDIT on one line, and the
   status word gets a slot of one width on every row: end-aligned words of
   two lengths ("Back in service" on the room under maintenance) slid EDIT
   52px along that one row, and the column of EDITs an owner scans stopped
   being a column. Only English needs the slot — the two Arabic verbs are
   the same length. */
.cols-rooms { grid-template-columns: minmax(11rem, 1fr) 10rem 18rem; }
html[lang="en"] .cols-rooms .acts [data-action="room-status"] { min-width: 14rem; text-align: end; }
/* On a phone the actions drop to a line of their own, and that line starts at
   EDIT — which is what the phone rule above always asked for, before the
   end-aligned .acts further down overrode it. The slot alone could not hold
   the column: "Take out for maintenance" sits right at 14rem, and a browser
   that sets it a few pixels wider slid EDIT on every row but one. */
@media (max-width: 620px) {
  .cols-rooms .acts { justify-content: flex-start; }
}

/* ---------------------------------------------------- button system (v3) --
   Kippo's two buttons and nothing else: ONE filled magenta action per view,
   a white ghost outline for everything beside it, and text buttons that are
   just words. Uppercase throughout; tracking only where the run is Latin. */
.btn {
  border-radius: var(--radius-md);
  padding-inline: var(--sp-5);
  font-weight: 700;
  transition: background 0.12s, border-color 0.12s, color 0.12s;
}
html[lang="en"] .btn { letter-spacing: var(--track-ui); }
.btn-primary {
  color: #fff; background: var(--color-accent); border: 1px solid var(--color-accent);
}
@media (hover: hover) { .btn-primary:hover:not(:disabled) { background: var(--color-accent-600); border-color: var(--color-accent-600); } }
.btn-primary:active:not(:disabled) { background: var(--color-accent-700); border-color: var(--color-accent-700); }
/* A primary that cannot be pressed yet is not a dimmer pink: a 45%-opacity
   magenta reads as a rendering fault. It drops to the ghost's ash outline
   until the bill balances, and the accent arrives the moment it can act —
   which is the one job the accent has. */
.btn-primary:disabled {
  opacity: 1; background: transparent; color: var(--color-neutral-600);
  border-color: var(--color-ash);
}

/* The browser's number spinner is a grey stepper from a different product
   inside a black field; amounts are typed or picked from the chips above. */
input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
/* The ghost is the border and nothing else — no fill, ever. */
.btn-ghost {
  color: var(--color-text); background: transparent;
  border: var(--hair); padding-inline: var(--sp-5);
}
@media (hover: hover) { .btn-ghost:hover:not(:disabled) { background: transparent; border-color: var(--color-accent); color: var(--color-accent); } }
/* Text buttons: ink, weight for emphasis, accent only where it is the point. */
.link { color: var(--color-neutral-300); font-weight: 700; text-transform: uppercase; }
@media (hover: hover) { .link:hover { color: var(--color-text); } }
/* The quiet word is the same word in a quieter grey — same case, same
   weight. Sentence case beside the caps made "EDIT Hide" and "CHANGE PIN
   Deactivate" look like two design systems sharing one cell. */
.link.quiet { color: var(--color-neutral-500); font-weight: 700; text-transform: uppercase; }
@media (hover: hover) { .link.quiet:hover { color: var(--color-neutral-200); } }
/* An action word is one control, so it is one line: "Take out for
   maintenance" folded into a two-line block that read as two. */
.tbl-row .acts .link { white-space: nowrap; }
.link.on { color: var(--color-accent); }
@media (hover: hover) { .link.on:hover { color: var(--color-accent-400); } }
/* Chips: ghost by default, filled magenta when chosen — the chosen chip is
   an action, so it earns the one fill. */
.chip { border-radius: var(--radius-md); color: var(--color-neutral-300); }
.chip.sel, .chip.on {
  color: #fff; background: var(--color-accent); border-color: var(--color-accent); font-weight: 700;
}
/* A chosen chip that cannot be changed — the room's unit while a session runs
   on it. The generic 45% dim over the magenta fill made a wine-red slab, the
   same rendering fault the disabled primary was rescued from. It cannot act,
   so it loses the accent; it is still the choice, so it keeps the white
   outline while the chips beside it stay dimmed. */
.chip.sel:disabled, .chip.on:disabled {
  opacity: 1; background: transparent;
  color: var(--color-text); border-color: var(--color-text); cursor: not-allowed;
}
/* The quiet half of a chip (the room type beside its name) stays readable on
   the fill: the grey it has on the void disappears into magenta. */
.chip.sel i, .chip.on i { color: inherit; font-weight: 400; }
/* The walkthrough bar: one filled next, one quiet exit. (These named a
   ".tour-bar" that the markup never had, and never applied.) */
.tourbar .btn-primary, .tourbar .link { min-height: 2.4rem; }
.tourbar .link { color: var(--color-neutral-500); font-weight: 400; }
@media (hover: hover) { .tourbar .link:hover { color: var(--color-neutral-200); } }

/* Actions sit at their natural width instead of filling the column. */
.row-actions .btn, .tbl-row .acts .btn { flex: 0 0 auto; }
.row-actions { display: flex; align-items: center; gap: var(--sp-5); justify-content: flex-end; }
/* Sheet footers: the confirm is filled, the cancel is a quiet neutral, and
   neither stretches edge to edge. */
.actions { justify-content: flex-end; }
.actions .btn { flex: 0 0 auto; justify-content: center; min-width: 7rem; }
.actions .btn-ghost { min-width: 5.5rem; }
/* The picker's add control: a ghost outline, filled magenta once chosen. */
.add {
  color: var(--color-text); background: transparent;
  border: var(--hair-quiet); border-radius: var(--radius-md);
  font-weight: 700; text-transform: uppercase;
}
@media (hover: hover) { .add:hover { background: transparent; border-color: var(--color-text); } }
.item.picked .add { color: #fff; background: var(--color-accent); border-color: var(--color-accent); }

/* A zero gap reads as a phrase, not a lonely 0 — and the unit never leads. */
.figure b { unicode-bidi: isolate; }

.rstack { display: inline-flex; flex-direction: column; gap: 1px; }
.rstack .rsub { font-size: var(--fs-md); font-weight: 500; color: var(--color-neutral-500); }

/* Isolate each line so a trailing digit can't reorder into the next one. */
.rstack > span, .rname .rtop, .rname .rtxt { unicode-bidi: isolate; }
.rstack > span { display: block; }

.art3d { max-width: 56px; max-height: 56px; }

/* The week strip was pushing past the card: grid children need min-width 0,
   and below tablet the row stacks instead of squeezing. Sideways the strip
   is clipped; upward it is not, so a value can never be cut through its
   digits — the labels used to be allowed to wrap as well, and "EGP 480" in a
   36px bar broke onto two lines whose top one the clip then took. */
main.own-dashboard .hero-row > *, main.own-undefined .hero-row > * { min-width: 0; }
.spark-wrap { min-width: 0; max-width: 100%; overflow-x: clip; overflow-y: visible; }
@media (max-width: 60rem) {
  main.own-dashboard .hero-row, main.own-undefined .hero-row {
    grid-template-columns: 1fr; gap: var(--sp-6);
  }
}

/* ------------------------------------------------------- the noted line --
   The one row in a list, the one bar in a chart and the one line in a
   breakdown that is worth the eye. It was briefly a second colour; it is
   white and bold now — never a state, never a button. The weight rules live
   in the figures block at the foot of this file. */

/* The next thing due: the soonest reservation, whoever is first in the queue,
   the booking a customer is counting down to. One row per list. */
.row.up-next .row-when b { color: var(--color-text); }

/* The biggest line in a breakdown — the number the owner came to find. */
.breakdown .brow.top b { color: var(--color-text); }

/* The best day in the reports chart. Today keeps the accent; it is the day
   being acted in. */

/* The top rate in the price list and the priciest line on the menu — the rows
   an owner scanning a list of numbers is actually looking for. */
.tbl-row.top .cell.strong { color: var(--color-text); }
/* The same rule in the reports tables: the room that earned most, the biggest
   bill of the period. Every table in the product marks its leader the same
   way, so the eye learns it once. */
.tbl-row.top .cell.strong.num { color: var(--color-text); }

/* ================================================================ figures ==
 * WHITE IS THE MONEY. MAGENTA IS THE ACTION. THERE IS NO THIRD COLOUR.
 *
 * Sunset Gold carried every non-money figure here for a day — counts, times,
 * dates, quantities — and the client asked for it out. So the palette is back
 * to what the reference specifies and nothing else: a black void, white
 * outlines and text, one magenta.
 *
 *     WHITE   — the money, and the words. Rates, totals, revenue, names.
 *     MAGENTA — what you can DO, plus the one session running right now.
 *     GREY    — the supporting figures, which now say their piece by being
 *               quiet rather than by being coloured.
 *
 * The standout of a set survives the removal, because it was never really
 * about hue: the top of a ranking, the next in a queue and the one that
 * breaks a run are marked by WEIGHT. Bold beside regular reads at a glance on
 * a black screen and costs the palette nothing.
 */

/* Money is white and loud — several of these would otherwise inherit the
   muted grey of the row they sit in, and money is what the owner opens the
   app for. */
.room .price,
.room .line b,
.breakdown .brow b,
.tbl-row .cell.strong,
.tbl-row .cell.money,
.line-row b,
.total-row b,
.price-big,
.picker-total .sum b,
.hero .amount b,
.figure b,
.stats .stat b,
.chart-val,
.spark-bar .spark-val,
.item .pr,
.row-end {
  color: var(--color-text);
}
/* A count or a percent in a row of figures keeps the money's size but not its
   colour: "2 sessions" and "29%" were as white as the takings beside them,
   and four equally loud numbers meant the money stopped standing out. */
.figure.qty b { color: var(--color-neutral-400); }
/* A room that earned nothing in the period is a quiet row of zeros, money
   columns included — a zero is not takings to shout about. */
.cols-report.muted .cell { color: var(--color-neutral-600); }
/* A duration in a breakdown is a figure to know, not money. */
.breakdown .brow .fig { color: var(--color-neutral-400); font-variant-numeric: tabular-nums; }

/* The standout of its set: weight, not colour. */
.row.up-next .row-when b,
.breakdown .brow.top b,
.tbl-row.top .cell.strong,
.chart-val.best,
.chart-labels span.best,
.spark-bar.best .spark-val {
  font-weight: 700;
}
/* The best day is a white bar in a row of ash ones — the same "louder, not
   another colour" move the rows above make. Today keeps the accent: it is
   the day being acted in, not merely the best one. */
.spark-bar.best,
.chart-bar.best { background: var(--color-text); }

/* The running session is the live thing on the screen. */
.room .timer,
.timer-big { color: var(--color-accent); }
.chart-val.today,
.spark-days span.today,
.chart-labels span.today { color: var(--color-accent); }
/* A promise inside the last half hour is about to need doing. */
.room .upnext.warn,
.room .next.soon { color: var(--color-accent); }

/* --------------------------------------------------------------- the mark --
   The brand mark, back beside the café's name by request (31-8-2026). It is a
   render inside a wireframe system and that is a knowing exception rather than
   an oversight: it is the product's only logo and the one thing on the screen
   allowed to be a picture of itself.

   Capped in px as well as em so a zoomed browser never upscales it past its
   native resolution — and so an uncapped natural-size logo can never push the
   header tall enough to cost the counter a row of rooms, which is exactly what
   happened the once these rules went missing. */
.brand { align-items: center; gap: var(--sp-3); }
.brand-mark { height: 2rem; max-height: 34px; width: auto; flex: 0 0 auto; }
footer .foot-mark, .demo-foot .foot-mark { height: 1.4rem; max-height: 22px; width: auto; vertical-align: -0.35em; }

/* ------------------------------------------------ the second look, 5-9-2026 --
   Every screen again, at a counter, a laptop and a phone, after the polish
   pass — and the things that were still wrong were mostly words in the
   wrong place rather than pixels. */

/* The reports' summary beside its hero: the same figure pattern the
   dashboard uses under its own hero, in one row, on every screen — three
   short amounts do not need the phone's two-column grid. */
.figures.inline { display: flex; flex-wrap: wrap; gap: var(--sp-4) var(--sp-7); }
.hero-row .figures.inline { padding-bottom: var(--sp-2); }
.hero-row .figures.inline .figure b { font-size: 1.4rem; }
@media (max-width: 620px) {
  .figures.inline { display: flex; }
}

/* "· minimum" beside the room figure on the session sheet, live: it goes
   out on its own the moment the minutes outgrow the floor (app.js). */
.figure .fnote { font-style: normal; color: var(--color-neutral-600); }
.figure .fnote[hidden] { display: none; }

/* The shift header, in classes — it was the one place the file still sized
   type in bare pixels. */
.shift-who { font-size: var(--fs-2xl); font-weight: 500; line-height: var(--lh-ui); }
.shift-since { margin-top: 2px; }
.shift-figures { margin: var(--sp-5) 0 var(--sp-4); }

/* The head row can carry a count note AND a trailing word — the dashboard's
   emergency stop lives there now, so it is never below the fold. The title
   never breaks either: frozen, the longer "Unfreeze café" squeezed TODAY
   until its pip sat alone on one line and the word on the next. */
.head .link { flex: 0 0 auto; }
.head h2 { white-space: nowrap; flex: 0 0 auto; }

/* "Last synced", on every owner screen. In the reports it closes the period
   row, since that row is the screen's head. */
.report-tabs .sync-stamp { margin-inline-start: auto; font-size: var(--fs-md); color: var(--color-neutral-500); }
/* Beside a count ("4 staff") it is a second note, not the same one. */
.head .count-note + .sync-stamp::before { content: "· "; }
@media (max-width: 620px) {
  .head .count-note + .sync-stamp::before { content: none; }
  /* A phone's head row has no room for it beside the title and the action,
     so it drops to a line of its own under them, and in the reports under
     the period row. */
  .head { flex-wrap: wrap; row-gap: 2px; }
  .head .sync-stamp, .report-tabs .sync-stamp { order: 3; flex-basis: 100%; margin-inline-start: 0; }
}

/* A frozen café, above everything on the owner's screens — the notice's own
   hairlines turned to the accent, because this is the state to undo. */
.frozen-band {
  margin: 0; font-weight: 500;
  border-top-color: var(--color-accent); border-bottom-color: var(--color-accent);
}

/* The laptop's install line carries the page's address to type on a phone. */
.install .ltr { unicode-bidi: isolate; color: var(--color-neutral-300); overflow-wrap: anywhere; }

/* ------------------------------------------------ the reports on a phone --
   The period header — tabs, the date, the hero, three figures, the sub-tabs
   — took four-fifths of the screen, so ROOMS and SESSIONS showed a row or
   nothing above the bottom bar: the long scroll the sub-tabs were split out
   to end (20-8-2026) was back. The date shrinks to its calendar button beside
   the tabs until a day is picked, the hero steps down (its accent outline
   stays: it is the one card that wears it), the three figures share a line,
   and the sub-tabs stay pinned while a month of sessions scrolls under them. */
@media (max-width: 620px) {
  main.own-reports .report-tabs { gap: var(--sp-2) var(--sp-5); }
  main.own-reports .range-pick ~ .day-pick:not(.sel) { width: 2.75rem; padding-inline: 0; justify-content: center; }
  main.own-reports .range-pick ~ .day-pick:not(.sel) .day-pick-text { display: none; }
  main.own-reports .hero.sm { padding: var(--sp-3) var(--sp-5); }
  main.own-reports .hero.sm .amount b { font-size: 2rem; }
  main.own-reports .hero-row .figures.inline { gap: var(--sp-3) var(--sp-5); }
  main.own-reports .hero-row .figures.inline .figure b { font-size: 1.1rem; }
  /* Pinned to the region's very top: a sticky box stops at the scroller's
     padding edge, which let a row of content show through above the tabs. */
  main.own-reports .mini-tabs.rtabs {
    position: sticky; top: calc(-1 * var(--sp-6)); z-index: 1;
    background: var(--color-bg); padding-top: var(--sp-2);
  }
  /* A session's time and its day read as one line above its room. */
  .sess-when .d { display: inline; margin-inline-start: var(--sp-3); }
}

/* The door — sign-up and sign-in (16-9-2026): the real app's /login in the
   demo's own clothes. One column, the text tabs the rest of the shell uses,
   and the one magenta on the quick-demo button, because on a shop window the
   thing to press is "let me in". */
.door { display: flex; justify-content: center; align-items: flex-start; padding-top: 6vh; }
.door-col { width: 100%; max-width: 40rem; margin-inline: auto; }
/* The language switch sits at the far end of the heading, where the
   chrome's own switch sits once somebody is in. */
.door-lang { margin-inline-start: auto; }
/* The shell's tab strip is inset by the shell's gutter; inside this column
   that put the first tab 28px in from the heading, the rule and every field
   under it, on the first screen anybody sees. */
.door-tabs { margin-top: var(--sp-3); padding-inline: 0; }
.door-form { display: block; }
.door-form .actions .btn { flex: 1 1 auto; }
.door-quick { margin-top: var(--sp-5); display: flex; flex-direction: column; gap: var(--sp-3); }
.door-quick .btn { width: 100%; justify-content: center; }
/* A credential the visitor has to type is one token: in the landing's phone
   frame "ps-cafe-dev" broke at its hyphen, "ps-cafe-" over "dev". */
.door-creds b { color: var(--color-neutral-300); font-weight: 500; white-space: nowrap; }
.door-note { margin-top: var(--sp-4); color: var(--color-neutral-600); }

/* Who is signed in, and the way out — in every shell (16-9-2026). A ghost
   button on a wide screen; on a phone the bar is text buttons only, so the
   sign-out keeps its word and joins them, with the same padding trick the
   other header links use to reach a thumb's 44px without moving the layout. */
.who { color: var(--color-neutral-400); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 12rem; }
/* On a laptop-width counter a long signed-in name gives way before the
   café's does (see .brand strong); its full text and number are its title. */
@media (min-width: 620.02px) and (max-width: 1439.98px) { .who { max-width: 8rem; } }
.btn-sm { min-height: 2rem; padding: var(--sp-2) var(--sp-3); font-size: var(--fs-sm); }
@media (pointer: coarse) { .btn-sm { min-height: 3rem; padding-inline: var(--sp-4); } }
@media (max-width: 620px) {
  .who { display: none; }
  .controls .btn-sm { border: 0; padding-inline: 0; min-height: 0; }
}
@media (max-width: 620px) and (pointer: coarse) {
  .controls .btn-sm { padding-block: 1.05rem; margin-block: -1.05rem; }
}

/* ------------------------------------------------------------------ stock --
 * The fridge's count and the menu's cost (18-8-2026, COMPETITOR_ANSWERS.md 04
 * and 05). Said the way the rest of the system says things: a level is a WORD
 * beside the name, bold among the grey flags because it is the standout of
 * the list — never a fill, never a badge colour. The one accent here is the
 * thing to do: Receive on a row whose shelf wants it (views-owner.js), and an
 * empty shelf in the picker, where the cashier has something to tell the
 * customer. The cost is money, so it is white — small, on the meta line, so
 * the price beside it is still the figure the row is for.
 */
.tbl-row .flagged.stock-flag { color: var(--color-neutral-300); font-weight: 700; }
.tbl-row .id .meta .cost { color: var(--color-text); }
/* Four verbs on a row that keeps a count — Receive, Stocktake, Edit, Hide —
   in a column wide enough for them on one line, still a fixed width so Edit
   and Hide stay one straight column down the table. Only when some item
   keeps a count: a café that tracks none keeps the narrower table. */
.tbl.with-stock .cols-menu { grid-template-columns: 1fr 7.9rem 22rem; }
html[lang="en"] .tbl.with-stock .cols-menu { grid-template-columns: 1fr 7.9rem 25rem; }
.notice.stock-watch b { color: var(--color-neutral-300); font-weight: 700; margin-inline-end: var(--sp-2); }
@media (max-width: 620px) {
  /* On a phone the row wraps, and the price used to sit wherever the name's
     meta line happened to end — beside "On hand 33 · Cost 9" on a bottle, a
     third of the way in on the tea — so the one figure the row is for moved
     from row to row. The name takes the room and the price keeps the row's
     far edge, one straight column down the list. */
  .tbl-row.cols-menu > .id { flex: 1 1 0; }
  .tbl-row.cols-menu > .cell.num { flex: 0 0 auto; }
  /* The head row is gone on a phone, so the rule under it drew a second line
     a gap below the restock notice's own. The notice's rule closes the top
     of the table instead. */
  .notice.stock-watch { margin-bottom: 0; }
  .notice.stock-watch + .tbl > .tbl-head + .hr { display: none; }
}

/* The picker's warning. A count is grey and bold (weight notes); the empty
   shelf is the accent (magenta acts). Neither hides the card. */
.item .stock-note { font-size: var(--fs-sm); line-height: var(--lh-ui); font-weight: 700; color: var(--color-neutral-400); }
.item .stock-note.out { color: var(--color-accent); }

/* The item sheet's price and cost, side by side and the same width; the
   fields line up at their foot however the two labels wrap. */
.pair.even { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: end; }
@media (max-width: 620px) { .pair.even { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }

/* The stock sheet's ledger: what happened and who, then the signed count —
   a quantity, so quiet grey, in its own left-to-right run. */
.ledger .line-row { align-items: center; }
.ledger .stack { display: inline-flex; flex-direction: column; gap: 2px; min-width: 0; }
.ledger .stack .meta { font-size: var(--fs-sm); color: var(--color-neutral-500); }
.ledger .delta {
  flex: 0 0 auto; font-size: var(--fs-lg); color: var(--color-neutral-400);
  font-variant-numeric: tabular-nums; unicode-bidi: isolate;
}

/* Best sellers carry their profit beside their takings: the word grey, the
   amount white like all money, both a step smaller than the takings. */
.brow-end { display: inline-flex; align-items: baseline; gap: var(--sp-4); flex: 0 0 auto; }
.brow-profit { font-style: normal; font-size: var(--fs-sm); color: var(--color-neutral-500); white-space: nowrap; }
.brow-profit .m { color: var(--color-text); font-variant-numeric: tabular-nums; }
.profit-block .profit-left > span { font-size: var(--fs-sm); }
/* A hint under a pair of fields keeps the air it has under a single one. */
.pair + .hint, .pair + .field-err { margin-top: var(--sp-2); }

/* ------------------------------------------- the period row's range, D63 --
   A custom range's two ends, there only while RANGE is the period: the
   single day's outline and its unseen date field, twice, each end's word
   quiet before its date. Neutral rather than the accent — the tab's mark
   already says which period this is — and the dates grey, as every figure
   that is not money is (section 6, 3b). */
.range-pick { display: inline-flex; align-items: center; gap: var(--sp-3); }
.day-pick.range-end { color: var(--color-neutral-300); }
.range-end .range-word { color: var(--color-neutral-500); }
@media (max-width: 620px) {
  /* Five periods on a phone. They take the row to themselves, so every one
     of them shows in Arabic, and where English still outgrows it the strip
     scrolls sideways under the bottom bar's fade (app.js marks the ends and
     brings the chosen one into view) rather than wrapping to a second row.
     Beside the calendar button, as D62 had it, "last month" was cut in half
     and "range" was off the screen altogether. Their outline on focus keeps
     a hair of room inside the scroller, which clips. */
  main.own-reports .mini-tabs.periods {
    flex: 1 1 100%; min-width: 0; flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
    padding: 3px; margin: -3px;
  }
  main.own-reports .mini-tabs.periods::-webkit-scrollbar { display: none; }
  main.own-reports .mini-tabs.periods button { flex: 0 0 auto; white-space: nowrap; }
  /* The sub-tabs gained EXPENSES, and in English the four outgrew the phone
     by a word, which wrapped alone onto a second line under the pinned row.
     Both rows step their gap down one — still twice a word's space, so
     "THIS MONTH LAST MONTH" reads as two tabs — and the sub-tabs scroll
     under the same fade too, should a count ever push them past the edge. */
  main.own-reports .mini-tabs.periods, main.own-reports .mini-tabs.rtabs { column-gap: var(--sp-4); }
  /* flex-shrink: 0 — a scroller inside the content column may otherwise be
     squeezed to nothing, since overflow drops its minimum height to zero. */
  main.own-reports .mini-tabs.rtabs { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; flex-shrink: 0; }
  main.own-reports .mini-tabs.rtabs::-webkit-scrollbar { display: none; }
  main.own-reports .mini-tabs.rtabs button { flex: 0 0 auto; white-space: nowrap; }
  /* The row under it was "last synced" alone; the day button joins it with
     its words back, so the header is no taller than D62 left it by more than
     the button's own touch height. While a range is the period its two ends
     lead that row, the day button steps down to its calendar, and "last
     synced" takes the line after. */
  main.own-reports .report-tabs .sync-stamp { flex-basis: auto; margin-inline-start: auto; }
  main.own-reports .range-pick ~ .sync-stamp { flex-basis: 100%; margin-inline-start: 0; }
  main.own-reports .report-tabs > .day-pick { flex: 0 0 auto; }
}

/* The expense list's head: its heading, and the one way to add to it. */
.expenses-head { display: flex; align-items: center; gap: var(--sp-4); }
.expenses-head .sub-head { margin: 0; }
.expenses-head .btn { margin-inline-start: auto; }
.expenses .breakdown + .helper { margin-top: var(--sp-2); }
@media (max-width: 620px) {
  /* Two lines an expense, not three: what it was for with its day and who
     under it, the amount at the row's far edge beside the name — one straight
     column of money down the list, as the menu's prices keep — and REMOVE
     under the amount, at the same edge, rather than alone on a third line. */
  .tbl-row.cols-expense:not(.tbl-head) {
    display: grid; grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "id amount" "id acts"; align-items: baseline; column-gap: var(--sp-4);
  }
  .tbl-row.cols-expense > .id { grid-area: id; }
  .tbl-row.cols-expense > .cell { grid-area: amount; justify-self: end; }
  .tbl-row.cols-expense > .acts { grid-area: acts; justify-self: end; margin-top: 0; }
}
