/* Senses console -- self-contained, no CDN, no build step.
 *
 * Palette carried over from the legacy app's sunrise-spa.css so the two systems
 * look like one family while they run side by side: Ink / Sand / Deep Teal / Gold.
 * Written by hand rather than pulled from a framework because there is no node
 * on cp7 and a first slice should not need an asset pipeline to be looked at.
 */
:root {
  --ink: #14202e;
  --ink-soft: #47566a;
  --sand: #faf7f2;
  --line: #e6e0d6;
  --teal: #0f8a86;
  --teal-dark: #0b6b68;
  --gold: #b8912f;
  --white: #fff;
  --danger: #b4433a;
  --ok: #2f7a4f;
  --radius: 10px;
  --shadow: 0 1px 2px rgba(20, 32, 46, .06), 0 8px 24px rgba(20, 32, 46, .05);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--sand);
  color: var(--ink);
  font: 15px/1.5 "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--teal-dark); }

/* ---------- shell ---------- */
/*
 * 🚨 THIS WAS `height: 60px` AND IT CLIPPED THE NAVIGATION ON EVERY PAGE (fixed 2026-08-12).
 *
 * 18 nav links need 72px at 1440px, 109px at 768px and 664px at 360px. A fixed 60px box with
 * `align-items: center` does not clip the overflow at the bottom — it centres it, so the nav started at
 * y = -302px on a phone and EIGHT of the eighteen links (Dashboard, Calendar, Invoices, Cash, Roster,
 * Stock, Purchasing, Catalogue — every daily-use screen) rendered above the viewport and could not be
 * reached at all, while the rest painted over the page body. At desktop it merely sliced the bottom off
 * the active pill, which is why it survived review.
 *
 * `min-height` + vertical padding lets the bar grow to whatever the nav needs. Nothing else depended on
 * the bar being exactly 60px once the calendar header stopped offsetting itself by that number — see
 * the .cal-head-cell note below.
 */
.topbar {
  /*
   * NOT `flex-wrap: wrap` on the bar itself — that was tried and made the bar 195px tall at 1440px,
   * because the brand, the nav and the user block each took a line of their own. The bar stays a single
   * row of three items; only the NAV wraps internally, so the bar grows to exactly the nav's height
   * (72px + padding ≈ 88px) instead of stacking everything.
   */
  display: flex; align-items: center; gap: 16px;
  padding: 8px 24px; min-height: 60px;
  background: var(--white); border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 10;
}
/* 🔑 `flex: none` + nowrap: the brand must NOT be the thing that gives way. Without it the nav (flex:1)
   took the slack and squeezed the brand instead — measured at 901px it wrapped to FIVE lines and pushed the
   bar to 128px. The nav is the element designed to wrap internally, so it absorbs the shortfall. */
.brand { display: flex; align-items: center; gap: 10px; font-weight: 700; letter-spacing: -.01em;
         flex: none; }
.brand > span { white-space: nowrap; }
.brand-mark {
  width: 30px; height: 30px; border-radius: 8px; flex: none;
  background: linear-gradient(135deg, var(--teal), var(--teal-dark));
  color: #fff; font-size: 15px; font-weight: 700;
  display: grid; place-items: center;
}
.brand small { display: block; font-weight: 500; font-size: 11px; color: var(--ink-soft); letter-spacing: .04em; text-transform: uppercase; }

/* ---------- brand logo (supplied "Senses Spa" artwork) ----------
 * The .brand-mark rule above is the placeholder "S" tile this replaced. It is kept because it is the
 * documented fallback if the image ever fails to load and someone needs the old lockup back in one edit.
 * object-fit: contain on both, so a future re-crop of the artwork can never stretch it.
 * The topbar takes the lotus only (30px, same footprint as the tile it replaced, so the 60px bar and
 * the nav beside it do not move); the login card takes the full logo including its wordmark.
 */
.brand-logo { width: 30px; height: 30px; flex: none; object-fit: contain; display: block; }
.login-card .brand { flex-direction: column; gap: 6px; }
.login-logo { width: 150px; height: auto; max-width: 100%; object-fit: contain; display: block; }
.nav {
  display: flex; gap: 2px; margin-left: 12px; flex-wrap: wrap;
  /* flex:1 + min-width:0 lets the nav take the slack and wrap INSIDE itself, so 18 links become
     two rows within the bar instead of forcing the bar's other children onto their own lines. */
  flex: 1 1 auto; min-width: 0;
}
.nav a {
  padding: 7px 13px; border-radius: 999px; text-decoration: none;
  color: var(--ink-soft); font-weight: 500; font-size: 14px;
}
.nav a:hover { background: var(--sand); color: var(--ink); }
.nav a[aria-current="page"] { background: var(--ink); color: #fff; }
/* ---------- mobile menu toggle ----------
 * The checkbox is OFF-SCREEN, not `display:none`: a hidden input cannot receive focus, and the keyboard
 * path is the only one some people have. Same pattern as the calendar's CSS-only tabs.
 */
.nav-toggle { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.navbtn { display: none; }        /* desktop shows the links themselves, so there is nothing to toggle */

.spacer { flex: 1; }
.whoami { font-size: 13px; color: var(--ink-soft); text-align: right; line-height: 1.3; }
.whoami strong { color: var(--ink); display: block; font-size: 14px; }

main { max-width: 1360px; margin: 0 auto; padding: 28px 24px 64px; }
.page-head { display: flex; align-items: flex-end; gap: 16px; margin-bottom: 20px; flex-wrap: wrap; }
h1 { margin: 0; font-size: 23px; letter-spacing: -.02em; }
.page-head p { margin: 4px 0 0; color: var(--ink-soft); font-size: 14px; max-width: 68ch; }
/* Actions in a page header belong at the far edge, not tucked against the subtitle. Scoped to a
   page-head that actually HAS them (no existing screen does), so no other header moves. */
.page-head > .row-actions { margin-left: auto; }

/* ---------- tiles ---------- */
.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; margin-bottom: 26px; }
.tile {
  background: var(--white); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 16px 18px; box-shadow: var(--shadow); text-decoration: none; color: inherit;
  display: block; transition: transform .12s ease, box-shadow .12s ease;
}
a.tile:hover { transform: translateY(-1px); box-shadow: 0 2px 4px rgba(20,32,46,.08), 0 12px 28px rgba(20,32,46,.08); }
.tile-value { font-size: 27px; font-weight: 700; letter-spacing: -.02em; }
.tile-label { font-size: 12px; color: var(--ink-soft); text-transform: uppercase; letter-spacing: .06em; margin-top: 2px; }

/* ---------- table card ---------- */
.card { background: var(--white); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; }
.card-head { padding: 14px 18px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 12px; }
.card-head h2 { margin: 0; font-size: 15px; }
.card-head .count { color: var(--ink-soft); font-size: 13px; }
.table-scroll { overflow-x: auto; }

/* ---------- the settings table FITS rather than scrolls ----------
 * 🚨 `th, td { white-space: nowrap }` is right for the invoices grid, where nine money columns genuinely
 * need a sideways scroll — and wrong here. With seven columns of policy prose the settings table was always
 * wider than the window, so `.table-scroll` hid its RIGHTMOST columns: the wired/not-wired status and the
 * Open button, i.e. the two things anybody opens this page to reach. Hany, 2026-08-13: "make the buttons
 * visible without scrolling, increase the row height to keep everything visible" — so the table wraps and
 * grows downwards instead of sideways.
 *
 * `table-layout: fixed` plus the colgroup in the view is what makes wrapping predictable: without it the
 * browser widens the description column to fit its longest sentence and the fit is lost again.
 */
.set-table { table-layout: fixed; }
.set-table th, .set-table td { white-space: normal; vertical-align: top; padding: 14px 14px; }
/* A 30-character snake_case key has no spaces to break at, so it would push the column wide on its own. */
/* `overflow-wrap: anywhere` breaks a key only when it genuinely cannot fit, where `break-all` split
   `default_turnaround_minutes` mid-word even when a whole line was free. 12px buys ~4 more characters. */
.set-table td.mono { overflow-wrap: anywhere; word-break: normal; font-size: 12px; line-height: 1.45; }
.set-table .hint { margin-top: 3px; }
/* The row's control keeps its own column and never wraps mid-label. */
.set-table td:last-child { text-align: right; }
.set-table td:last-child .btn { white-space: nowrap; }
/*
 * The register carries seven columns, and at 768 they came to 736px inside a 718px card — 18px over, so
 * the row's own action was the thing hanging past the edge. The column that goes is the machine KEY:
 * `allow_partial_refund` is for an API caller and a support ticket, and the plain-English name of the
 * setting is sitting immediately beside it. It is still on the setting's own page, in the subtitle.
 */
@media (max-width: 900px) {
  .set-table th:nth-child(2), .set-table td:nth-child(2) { display: none; }
  /*
   * ⚠️ HIDING THE CELLS IS NOT ENOUGH. The table is `table-layout: fixed` over a `<colgroup>`, and a
   * colgroup does not care that a column's cells are gone — the declared widths still add up to 736px
   * inside a 718px card, so the row's own action kept hanging past the edge. Between the card
   * breakpoint and here the table sizes itself from what is left.
   */
  .set-table { table-layout: auto; }
  .set-table col { width: auto; }
  /*
   * 🚨 AND THE VALUES NEED A PLACE TO BREAK. A json setting is printed as its literal, and the receipt
   * watermark map is {"voided":"VOID","refunded":"REFUNDED"} — 315px with not one space in it. Wrapping
   * alone cannot help a string with no break opportunity, so that single cell set its column at 322px and
   * pushed the Receipt group 22px past its card in the 721-768px band. Breaking mid-token lets it
   * break mid-token; every other value here is short enough to be untouched, and above 900px the column
   * is wide enough that nothing wraps at all.
   */
  .set-table td[data-label] { white-space: normal; overflow-wrap: anywhere; }
}

/* ---------- the settings table becomes CARDS on a phone ----------
 * 🚨 A SEVEN-COLUMN TABLE CANNOT BE MADE TO WORK AT 390px, and pretending otherwise is what the fixed layout
 * did: measured at 390 the columns came out 105/68/41/41/31/31/24px, so every cell wrapped into a sliver, a
 * row stood 128px tall, and the last column — the Open button, the only control on the row — was 24px wide
 * and NOT REACHABLE without scrolling the wrapper sideways. 67 tap targets under 32px.
 *
 * So below 720px each row stops being a row and becomes a card: the setting's name and description across the
 * top, its fields underneath as labelled pairs (the labels come from `data-label` on the cells, so they live
 * with the data), and the control full width at the bottom where a thumb expects it.
 */

/* ---------- .cards-xs: the same cards, for a table that is NOT six columns of prose ----------
 * .cards-sm becomes cards at 1280px because the users grid genuinely needs it there. The roles grid is
 * five compact columns, three of them plain counts, and it fits down to about 560px — so borrowing
 * .cards-sm handed a 1280px LAPTOP the phone layout and turned a 496px table into 1,559px of stacked
 * cards. Same treatment, declared at this block  720px boundary instead. */
@media (max-width: 720px) {
  .set-table, .set-table tbody, .set-table tr, .set-table td,
  .cards-sm, .cards-xs, .cards-sm tbody, .cards-xs tbody, .cards-sm tr, .cards-xs tr, .cards-sm td, .cards-xs td { display: block; width: auto; }
  .set-table thead, .cards-sm thead, .cards-xs thead { display: none; }          /* the labels move into the cells */
  .set-table colgroup, .cards-sm colgroup, .cards-xs colgroup { display: none; }       /* fixed widths are meaningless once cells are blocks */
  .set-table, .cards-sm, .cards-xs { table-layout: auto; }

  .set-table tr, .cards-sm tr, .cards-xs tr {
    border-bottom: 1px solid var(--line); padding: 14px 14px 12px;
  }
  .set-table tr:last-child, .cards-sm tr:last-child, .cards-xs tr:last-child { border-bottom: 0; }

  .set-table td, .cards-sm td, .cards-xs td { border: 0; padding: 0; }
  .set-table td.set-name { font-weight: 600; font-size: 15px; margin-bottom: 10px; }
  .set-table td.set-name .hint { font-weight: 400; }

  /* Labelled pairs: name on the left, value hard right, so the values line up down the card. */
  .set-table td[data-label], .cards-sm td[data-label], .cards-xs td[data-label] {
    display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
    padding: 5px 0; font-size: 13.5px; text-align: left;
  }
  .set-table td[data-label]::before, .cards-sm td[data-label]::before, .cards-xs td[data-label]::before {
    content: attr(data-label); color: var(--ink-soft); flex: none;
  }
  .set-table td.mono[data-label] { font-size: 12px; }
  /* The first cell of a card is its title, and the actions sit at the bottom, full width. */
  .cards-sm td:first-child, .cards-xs td:first-child { font-weight: 600; font-size: 15px; margin-bottom: 8px; }
  .cards-sm td.row-actions, .cards-xs td.row-actions { margin-top: 12px; text-align: left; }
  .cards-sm td.row-actions .btn, .cards-xs td.row-actions .btn { display: inline-block; padding: 10px 14px; }
  .cards-sm td[data-label], .cards-xs td[data-label] { white-space: normal; }

  /* The control gets its own row and a real tap target, rather than a 24px column. */
  .set-table td.set-open { margin-top: 12px; }
  .set-table td.set-open .btn {
    display: block; width: 100%; text-align: center; padding: 11px 14px; font-size: 15px;
  }
}
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th, td { padding: 11px 18px; text-align: left; border-bottom: 1px solid var(--line); white-space: nowrap; }
th { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-soft); font-weight: 600; background: #fcfbf8; }
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover { background: #fcfbf8; }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
td.wrap { white-space: normal; min-width: 220px; }
.muted { color: var(--ink-soft); }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; }

/* ---------- pills ---------- */
.pill { display: inline-block; padding: 2px 9px; border-radius: 999px; font-size: 12px; font-weight: 600; }
.pill-on { background: #e6f4ec; color: var(--ok); }
.pill-off { background: #f3f0eb; color: var(--ink-soft); }
.pill-gold { background: #fbf3e0; color: var(--gold); }
.pill-none { background: #fdecea; color: var(--danger); }

/* ---------- login ---------- */
.login-wrap { min-height: 100vh; display: grid; place-items: center; padding: 24px; }
.login-card { width: 100%; max-width: 380px; background: var(--white); border: 1px solid var(--line); border-radius: 14px; box-shadow: var(--shadow); padding: 30px; }
.login-card .brand { justify-content: center; margin-bottom: 6px; }
.login-card h1 { font-size: 19px; text-align: center; margin-bottom: 2px; }
.login-card .sub { text-align: center; color: var(--ink-soft); font-size: 13px; margin: 0 0 22px; }
label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; }
/*
 * 🚨 `type=email` WAS STYLED NOWHERE, so every email field fell back to the browser default and
 * looked nothing like the text input beside it — visible on the profile page and on the admin user
 * form. tel and url are included now too: the next form that needs one should not have to rediscover
 * this. Checkboxes, radios, buttons and file inputs are deliberately excluded — they are not boxes.
 */
/* ── FORM CONTROLS: ONE canonical design ──────────────────────────────────────────────────────────
   Every box-like control shares the design used on the profile page, which is the one Hany picked as the
   reference. Before this there were THREE competing looks and the same text input changed shape depending
   on where it sat: 10px/12px padding bare, 9px/11px inside `.field` (more specific, so it won on the
   profile page), and 8px/11px at 14px on a white ground in filter bars and the calendar. `number`, `date`,
   `time`, `datetime-local` and `search` — 63 uses between them — were styled ONLY inside those wrappers
   and rendered as raw browser controls anywhere else, which is what made the discount amount field look
   nothing like the reason field beside it.

   Checkboxes, radios, buttons, file and hidden inputs are deliberately excluded: they are not boxes. */
input[type=text], input[type=password], input[type=email], input[type=tel], input[type=url],
input[type=number], input[type=date], input[type=time], input[type=datetime-local],
input[type=search], input[type=month], input[type=week], select, textarea {
  padding: 9px 11px; font: inherit; font-size: 15px;
  border: 1px solid var(--line); border-radius: 8px; background: #fdfcfa; color: var(--ink);
}

/* WIDTH is separate from appearance on purpose. Inside a `.field` a control fills its column, which is
   what every form on the system wants. Inline contexts — filter bars, the calendar's date nav, an input
   welded to a unit tag — keep their natural width, because forcing 100% there turns a row of filters into
   a stack. Matching the DESIGN does not mean matching the width. */
.field input:not([type=checkbox]):not([type=radio]):not([type=file]):not([type=hidden]),
.field select, .field textarea { width: 100%; }

textarea { min-height: 92px; resize: vertical; }

/* ---------- `cards-sm` needs a WIDER boundary than the settings table ----------
 * Measured: with its six columns the users table needs ~1,243px to fit, so at 768px it still scrolled
 * sideways and the row's four actions still sat off the edge. The settings table fits from ~1,000px, which is
 * why the two share the card rules but not the breakpoint — a boundary is a measurement, not a convention.
 */
@media (max-width: 1280px) {
  .cards-sm, .cards-sm tbody, .cards-sm tr, .cards-sm td { display: block; width: auto; }
  .cards-sm thead, .cards-sm colgroup { display: none; }
  .cards-sm { table-layout: auto; }
  .cards-sm tr { border-bottom: 1px solid var(--line); padding: 14px 14px 12px; }
  .cards-sm tr:last-child { border-bottom: 0; }
  .cards-sm td { border: 0; padding: 0; white-space: normal; }
  .cards-sm td:first-child { font-weight: 600; font-size: 15px; margin-bottom: 8px; }
  .cards-sm td[data-label] {
    display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
    padding: 5px 0; font-size: 13.5px; text-align: left;
  }
  /* 🔑 `margin-right: auto` ON THE LABEL, NOT `space-between` ON THE ROW. Every text node in a flex
     container becomes its own anonymous flex item, so a cell holding "91.00" plus a <span>EUR</span> was
     spread into three columns — the amount adrift in the middle and its currency pinned to the far edge.
     Pushing everything off the label keeps the value and its unit together, whatever a cell is made of. */
  .cards-sm td[data-label]::before {
    content: attr(data-label); color: var(--ink-soft); flex: none; margin-right: auto;
  }
  /* A long value must not share a row with its label: the outlets cell is text PLUS a "+N more" span, and
   * space-between would push that span to the far right where it reads as a second value. Stack instead. */
  .cards-sm td.u-outlets { display: block; padding: 5px 0; }
  .cards-sm td.u-outlets::before {
    content: attr(data-label); display: block; color: var(--ink-soft); font-size: 13.5px; margin-bottom: 2px;
  }
  .cards-sm td.row-actions { margin-top: 12px; text-align: left; }
  .cards-sm td.row-actions .btn { display: inline-block; padding: 10px 14px; }
}

/* 🚨 A LIST INSIDE A CELL MUST WRAP. `td { white-space: nowrap }` is right for money columns and wrong for a
   list of 18 outlet names: it produced a 2,488px cell and a 3,551px table that scrolled sideways on a 1440px
   desktop, pushing every row's actions off the edge. Wrapping applies at ALL widths, not just mobile. */
.u-outlets { white-space: normal; min-width: 180px; max-width: 340px; }

/* ---------- searchable select ----------
 * The control the JS builds must be INDISTINGUISHABLE from the native one beside it, so `.ss-btn` and
 * `.ss-search` take the same padding, border, radius, background and font as the block above rather than
 * approximating it. Hany chose this over bootstrap-select, which needs jQuery and Bootstrap and would have
 * restyled every select on the estate.
 *
 * 🔑 `.ss-native` is `position: absolute; opacity: 0` — NOT `display: none`. A display-none select is still
 * submitted, but it cannot be focused, so the browser cannot scroll to it or show its own validation bubble
 * when a `required` select is empty. Keeping it in the layer but invisible preserves both.
 */
.ss { position: relative; }
.ss-native { position: absolute; left: 0; bottom: 0; width: 100%; height: 1px; opacity: 0; pointer-events: none; }

.ss-btn {
  display: block; width: 100%; text-align: left; cursor: pointer;
  padding: 9px 30px 9px 11px; font: inherit; font-size: 15px;
  border: 1px solid var(--line); border-radius: 8px; background: #fdfcfa; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The chevron, drawn rather than an image so it needs no asset and inherits the ink colour. */
.ss-btn::after {
  content: ''; position: absolute; right: 12px; top: 50%; width: 7px; height: 7px;
  margin-top: -5px; border-right: 1.6px solid var(--ink-soft); border-bottom: 1.6px solid var(--ink-soft);
  transform: rotate(45deg); pointer-events: none;
}
.ss-btn:hover { border-color: var(--teal); }
.ss-btn[aria-expanded="true"] { border-color: var(--teal); outline: 2px solid rgba(15,138,134,.35); outline-offset: 1px; }

.ss-panel {
  position: absolute; z-index: 40; top: calc(100% + 4px); left: 0; right: 0;
  background: var(--white); border: 1px solid var(--line); border-radius: 10px;
  box-shadow: 0 12px 26px rgba(23,43,42,.14); padding: 8px;
  /* 🔑 The panel may be WIDER than the field. Outlet names run to "Diamond Beach Senses SPA" and a panel
     locked to the field's width truncated the very text you are reading to choose between. It grows to its
     content, floors at the field's width and is capped so it never leaves the viewport. */
  width: max-content; min-width: 100%; max-width: min(460px, calc(100vw - 24px));
}
.ss-search { width: 100%; margin-bottom: 6px; }
.ss-list { max-height: 260px; overflow-y: auto; -webkit-overflow-scrolling: touch; }
/* `position: relative` + right padding so the tick sits in its own gutter. As part of the text it was
   swallowed by the ellipsis on exactly the rows that needed it — "Alma Bay Senses S…" with the mark lost. */
.ss-opt {
  position: relative; padding: 9px 26px 9px 11px; border-radius: 7px; cursor: pointer; font-size: 14.5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ss-opt:hover, .ss-opt.is-active { background: var(--sand); }
.ss-opt[aria-selected="true"] { font-weight: 600; }
.ss-opt[aria-selected="true"]::after {
  content: '✓'; position: absolute; right: 9px; top: 9px; color: var(--teal);
}
.ss-opt[aria-disabled="true"] { color: var(--ink-soft); cursor: default; }
.ss-empty { margin: 6px 0 2px; padding: 6px 11px; color: var(--ink-soft); font-size: 13.5px; }

/* A finger needs a bigger row than a mouse, and the panel gets the screen's height rather than 260px. */
@media (max-width: 900px) {
  .ss-opt { padding: 12px 11px; font-size: 15px; }
  .ss-list { max-height: 50vh; }
}

/* select and textarea got no focus ring at all before — only `input` was covered. */
input:focus, select:focus, textarea:focus {
  outline: 2px solid rgba(15,138,134,.35); outline-offset: 1px; border-color: var(--teal);
}
input::placeholder, textarea::placeholder { color: #9aa3ae; }
/* A disabled control has to look unavailable, not merely refuse to work. */
input:disabled, select:disabled, textarea:disabled {
  background: var(--sand); color: var(--ink-soft); cursor: not-allowed;
}
.field { margin-bottom: 16px; }
.check { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ink-soft); margin-bottom: 20px; }
.check input { accent-color: var(--teal); }
button.primary {
  width: 100%; padding: 11px 16px; font-size: 15px; font-weight: 600; font-family: inherit;
  color: #fff; background: var(--teal); border: 0; border-radius: 8px; cursor: pointer;
}
button.primary:hover { background: var(--teal-dark); }
.alert { background: #fdecea; border: 1px solid #f5c6c2; color: #8f332c; padding: 10px 12px; border-radius: 8px; font-size: 13px; margin-bottom: 18px; }
/* .alert is styled as an ERROR (red). A success panel needs its own variant, or a temporary password
   handed over successfully is announced in the colour of a failure. */
.alert-ok { background: #e6f4ec; border-color: #bfe3ce; color: #1f5c3d; }
.alert-ok code { background: rgba(31,92,61,.08); padding: 2px 8px; border-radius: 6px; }
.logout { background: none; border: 0; color: var(--ink-soft); font: inherit; font-size: 13px; cursor: pointer; padding: 0; text-decoration: underline; }

.empty { padding: 34px 18px; text-align: center; color: var(--ink-soft); font-size: 14px; }
footer.foot { max-width: 1360px; margin: 0 auto; padding: 0 24px 40px; color: var(--ink-soft); font-size: 12px; }

/* ---------- settings screens ---------- */
.filterbar { display: flex; gap: 8px; align-items: center; margin-bottom: 18px; flex-wrap: wrap; }

.filterbar input[type=search] { min-width: 240px; }
.btn {
  display: inline-block; padding: 8px 14px; font: inherit; font-size: 14px; font-weight: 500;
  border: 1px solid var(--line); border-radius: 8px; background: var(--white); color: var(--ink);
  cursor: pointer; text-decoration: none; white-space: nowrap;
}
.btn:hover { background: var(--sand); }
.btn-primary { background: var(--teal); border-color: var(--teal); color: #fff; }
.btn-primary:hover { background: var(--teal-dark); border-color: var(--teal-dark); }
.btn-danger { color: var(--danger); border-color: #f0cdc9; background: #fdf5f4; }
.btn-danger:hover { background: #fbe9e7; }
.btn-plain { border-color: transparent; background: none; color: var(--ink-soft); text-decoration: underline; }
.btn-sm { padding: 5px 10px; font-size: 13px; }

.notice {
  background: #e9f5ef; border: 1px solid #bfe0cd; color: #1f5e3c;
  padding: 10px 14px; border-radius: 8px; font-size: 14px; margin-bottom: 18px;
}
.notice-info { background: #eef4fb; border-color: #c9dcf0; color: #24486e; }

.pill-money { background: #fbf3e0; color: var(--gold); margin-left: 6px; }
.hint { font-size: 12px; display: block; margin-top: 3px; font-weight: 400; }
.crumb { margin: 0 0 4px; font-size: 13px; color: var(--ink-soft); }
.crumb a { color: var(--ink-soft); }

.two-col { display: grid; grid-template-columns: minmax(300px, 380px) 1fr; gap: 18px; align-items: start; }
@media (max-width: 1080px) { .two-col { grid-template-columns: 1fr; } }
.pad { padding: 18px; }
.big-value { font-size: 30px; font-weight: 700; letter-spacing: -.02em; margin: 0 0 18px; }
.big-value .unit { font-size: 15px; font-weight: 500; color: var(--ink-soft); }
h3.sub, h4.sub { font-size: 13px; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-soft); margin: 24px 0 8px; }
.setform { border-top: 1px solid var(--line); padding-top: 16px; }
.input-unit { display: flex; align-items: stretch; }
.input-unit input { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.unit-tag {
  display: grid; place-items: center; padding: 0 11px; font-size: 13px; color: var(--ink-soft);
  background: var(--sand); border: 1px solid var(--line); border-left: 0;
  border-radius: 0 8px 8px 0;
}
/* The per-type `.field` appearance rule that used to live here is gone: it duplicated the canonical rule
   above and was the reason a text input changed padding depending on its wrapper. Width is handled there. */

table.timeline { font-size: 13px; }
table.timeline th, table.timeline td { padding: 7px 10px; }
table.timeline tr.is-current td { background: #f4faf6; }
table.timeline tr.is-future td { background: #fdf8ec; }

details > summary { cursor: pointer; list-style: none; display: inline-block; }
details > summary::-webkit-details-marker { display: none; }
details[open] > summary { background: var(--sand); }
.pop {
  margin-top: 10px; padding: 14px; text-align: left; background: #fcfbf8;
  border: 1px solid var(--line); border-radius: 10px; min-width: 260px;
}

/* ---------- calendar (day view) ---------- */
.cal-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; flex-wrap: wrap; }
/*
 * 🚨 THE PRIMARY ACTION WAS BEING STRANDED. Below roughly 1400px the bar ran out of room and wrapped
 * wherever it happened to run out — which put "Add booking" alone on a second line, left-aligned under
 * the outlet picker, while the status filter kept the first line. The filter is the widest optional
 * thing here, so IT is what moves: given a full-width basis it takes a line of its own and the button
 * stays where the eye looks for it, at the right of the first row. Ordered explicitly rather than left
 * to the wrap point, so there is no width at which the bar rearranges itself into a third shape.
 */
/*
 * The old wrap-protection rule lived here: below 1400px it gave the status filter a full-width row of
 * its own so "Add booking" could not be pushed off the first line. The filter is no longer IN the bar —
 * it sits in `.cal-subbar` with the day's facts — so the toolbar is six short controls that fit on one
 * line at every width worth naming, and the hack that only worked below 1400px is gone with it.
 */
.cal-subbar {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px 16px;
  flex-wrap: wrap; margin: 0 0 10px;
}
.cal-subbar .cal-meta { margin: 0; text-align: right; }
.cal-subbar .legend { flex: 0 1 auto; }
@media (max-width: 720px) {
  /* Stacked, and the facts read left-to-right like everything else once they are on their own line. */
  .cal-subbar { flex-direction: column; align-items: stretch; }
  .cal-subbar .cal-meta { text-align: left; }
}
/*
 * A lane that is only on screen because it HOLDS work: `require_roster` is on and this therapist has no
 * published shift. Its treatments must be as legible as any other — hiding them is the bug this exists
 * to fix — so only the empty background is muted, marking the lane as closed for new bookings.
 */
.cal-col.is-unrostered .cal-slot { background: repeating-linear-gradient(135deg,
  transparent, transparent 5px, rgba(20,32,46,.035) 5px, rgba(20,32,46,.035) 10px); cursor: default; }
.cal-col.is-unrostered .cal-head-cell .pill { margin-left: 6px; }

/*
 * The holding bay for work nobody is on yet. Tinted so it reads as a staging area rather than a person,
 * and its slots are inert — you cannot book "no therapist", you drag out of here onto someone.
 */
/* The currency picker sits inside a section heading, so it must not inherit the heading's weight. */
.sec-head .sec-ccy { display: inline-block; margin-left: 10px; vertical-align: middle; }
.sec-head .sec-ccy select { font: inherit; font-size: 12px; font-weight: 500; text-transform: none;
  letter-spacing: 0; padding: 2px 6px; border: 1px solid var(--line); border-radius: 6px;
  background: var(--white); color: var(--ink); }

.cal-col.is-unassigned { background: #fbf7ef; }
/*
 * Destroying a bill must not sit flush under taking money for it. A rule and a gap say "different kind
 * of thing" without hiding the control from anyone who needs it.
 */
.row-actions.danger-zone {
  margin-top: 22px; padding-top: 14px; border-top: 1px dashed var(--line);
}
.cal-col.is-unassigned .cal-head-cell { color: var(--ink-soft); font-style: italic; }
.cal-col.is-unassigned .cal-slot { cursor: default; }

/*
 * An appointment listed UNDER the charge it belongs to, on the bill. An indent alone did not read as a
 * hierarchy — 22px against the parent cell's own 14px is a nudge, not a relationship — so the row is
 * tinted and carries a rule down the left, and the last one closes the group.
 */
tr.sub-row > td {
  padding-left: 30px; position: relative; background: #fbfaf7;
  border-top: 0; font-size: 12.5px;
}
tr.sub-row > td::before {
  content: ''; position: absolute; left: 14px; top: 0; bottom: 0; width: 2px; background: var(--line);
}
tr.sub-row:last-child > td::before { bottom: 50%; }

/*
 * 🚨 PINNED TO THE VIEWPORT BY THE SCRIPT IN THE LAYOUT, because an absolutely-positioned panel is
 * clipped by `.table-scroll` (overflow-x: auto → overflow-y: auto) and by `.card` (overflow: hidden).
 * Placed correctly and still sliced in half is what "need scrolling to see" turned out to mean. `right`
 * and `bottom` are cleared because the script sets `top`/`left` outright.
 */
.row-more > .pop.is-pinned { position: fixed; right: auto; bottom: auto; margin: 0; }
/*
 * And a floor under both cases: a panel taller than the window fits in NEITHER direction — the rooms
 * editor is 390px and a 700px laptop window has 372px above the row and 328px below it, so flipping it
 * only moves which end is cut off. Bounded to the viewport and scrolled inside itself, every panel is
 * reachable whatever the screen.
 */
.row-more > .pop { max-height: calc(100vh - 140px); overflow-y: auto; }
/* No transition on the pinned panel: it is repositioned on every scroll frame, and an animated `top`
   would make it swim behind its own row. */
.row-more > .pop.is-pinned { transition: none; }

/* ── Searching the day (2026-08-18) ────────────────────────────────────────────────────────────────
   A treatment that does not match is removed from the grid rather than dimmed: the point is to see the
   one reservation being looked for, and a greyed block still occupies a column. */
.cal-ev.cal-nomatch { display: none; }
.cal-find { display: flex; align-items: center; gap: 6px; position: relative; }
.cal-find input {
  width: 260px; max-width: 40vw; padding: 7px 10px; font: inherit; font-size: 13px;
  border: 1px solid var(--line); border-radius: 999px; background: var(--white); color: var(--ink);
}
.cal-find input:focus { outline: 2px solid var(--teal); outline-offset: 1px; }
.cal-find-x {
  position: absolute; left: 232px; width: 22px; height: 22px; padding: 0; line-height: 1;
  border: 0; border-radius: 50%; background: var(--sand); color: var(--ink-soft); cursor: pointer;
}
.cal-find-x:hover { color: var(--ink); }
.cal-find-n { font-size: 12px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
@media (max-width: 900px) {
  .cal-find { flex: 1 1 100%; order: 4; }
  .cal-find input { width: 100%; max-width: none; }
  .cal-find-x { left: auto; right: 46px; }
}

/* A day whose treatments are all filtered out shows an empty grid; without this, nothing on the page
   explains why, and an empty calendar reads as a broken one. */
.cal-none {
  margin: 10px 0 0; padding: 10px 12px; border: 1px dashed var(--line); border-radius: 8px;
  background: #fcfbf8; color: var(--ink-soft); font-size: 13px;
}

.cal-nav { display: flex; gap: 6px; align-items: center; }
/*
 * ── The header, 2026-08-18 ────────────────────────────────────────────────────────────────────────
 * 🚨 IT SAID EVERYTHING TWICE. The outlet was named in the picker AND opening the summary line; the day
 * was stated as `08/14/2026` in the picker AND "Fri 14 Aug 2026" beside it, in two formats, at the same
 * size — so nothing on the bar looked more important than anything else and the whole thing read as a
 * row of seven equal controls. The outlet name is gone from the summary, and the day is now stated once
 * as a heading with its picker demoted to what it is: a control.
 *
 * The vertical cost matters here more than on other screens: every pixel the header takes is an hour of
 * the day the grid cannot show. At 820px the bar and its summary were pushing the grid to y=341 with
 * only 498px of day left under it.
 */
.cal-title { font-size: 19px; font-weight: 600; letter-spacing: -.01em; margin-right: 2px; }
/* The pickers are controls, not statements — smaller and quieter than the day they change. */
.cal-nav input[type="date"], .cal-controls select {
  font-size: 13px; color: var(--ink-soft); padding: 6px 8px;
}
.cal-nav input[type="date"]:focus, .cal-controls select:focus { color: var(--ink); }
/*
 * 🔑 THE PICKER STOPS REPEATING THE DAY. "08/14/2026" sat directly beside "Fri 14 Aug 2026" — the same
 * fact twice, in two formats, a centimetre apart. The heading keeps the statement; the input keeps only
 * its calendar affordance, so it reads as "jump to another day" rather than a second opinion about
 * which day this is. Webkit-only: a browser that ignores this simply shows the date as it does today,
 * which is what the screen looked like before.
 */
.cal-nav input[type="date"] { width: 42px; padding: 6px 4px; }
.cal-nav input[type="date"]::-webkit-datetime-edit,
.cal-nav input[type="date"]::-webkit-inner-spin-button,
.cal-nav input[type="date"]::-webkit-clear-button { display: none; }
.cal-nav input[type="date"]::-webkit-calendar-picker-indicator {
  padding: 0; margin: 0 auto; opacity: .55; cursor: pointer;
}
.cal-nav input[type="date"]:hover::-webkit-calendar-picker-indicator { opacity: 1; }
@supports not (selector(::-webkit-datetime-edit)) {
  .cal-nav input[type="date"] { width: auto; padding: 6px 8px; }
}
.cal-bar { margin-bottom: 10px; }
.cal-meta { font-size: 13px; margin: 0 0 10px; }
.cal-meta b { color: var(--ink); font-weight: 600; }
.legend { display: flex; gap: 8px; font-size: 12px; color: var(--ink-soft); flex-wrap: wrap; }
.legend span { display: inline-flex; align-items: center; gap: 5px; }

/*
 * ── The key is the filter (2026-08-14, Hany) ────────────────────────────────────────────────────────
 * It already named every status and coloured every swatch; it just did not do anything. Pressing one
 * now shows or hides that status in BOTH grids. Cancelled starts hidden — a cancelled treatment is not
 * work anyone is looking at, and it was the single biggest source of clutter on a busy day.
 */
.lg-btn {
  display: inline-flex; align-items: center; gap: 5px; cursor: pointer;
  font: inherit; color: inherit; background: var(--white);
  border: 1px solid var(--line); border-radius: 999px; padding: 3px 10px 3px 7px;
  transition: opacity .12s ease, background .12s ease, border-color .12s ease;
}
.lg-btn:hover { border-color: var(--ink-soft); }
.lg-btn:focus-visible { outline: 2px solid var(--brand, #0E7C6B); outline-offset: 1px; }
/* Off reads as off at a glance: drained colour, struck label, but still legible and still clickable. */
.lg-btn[aria-pressed="false"] { opacity: .5; background: transparent; text-decoration: line-through; }
.lg-btn[aria-pressed="false"] .sw { filter: grayscale(1); }
/* The count is the reason a filtered day does not read as an empty one. Tabular figures so the row of
   buttons does not jiggle as the numbers change from day to day. */
.lg-n { font-variant-numeric: tabular-nums; opacity: .65; font-size: 11px; }

/*
 * ── A clash, shown against the row that causes it (2026-08-14) ──────────────────────────────────────
 * The booking form checks each treatment row against the therapist, the room and opening hours as it is
 * typed. Amber, not red: the slot is refusable, not yet refused — the save is still the authority, and a
 * clerk who knows better (the guest is already here) must not feel blocked from trying.
 */
/* ── The appointments a line implies (2026-08-18) ──────────────────────────────────────────────────
   A quantity of 3, or a package of four sessions, is several appointments and each can be moved to its
   own day, time, therapist and room. Indented under its row and ruled down the left so it reads as
   "belonging to the line above" rather than as four more lines to fill in. Same grid discipline as
   `.line-main`: one appointment is ONE row at every width, because the fault this screen keeps coming
   back with is a control wrapping onto a line of its own. */
.line-sessions { margin: 10px 0 2px 12px; padding-left: 12px; border-left: 2px solid var(--line); }
.sessions-head {
  font-size: 12px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--ink-soft);
}
/* Folded away behind its own summary: a booking of four treatments was twelve rows of controls deep.
   The summary still states the schedule, so shutting it hides the controls and not the facts. */
.sessions-fold > summary, .pkg-fold > summary {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  cursor: pointer; padding: 3px 0 6px; list-style: none;
}
.sessions-fold > summary::-webkit-details-marker,
.pkg-fold > summary::-webkit-details-marker { display: none; }
/* The affordance, drawn rather than relying on the browser's triangle, which the reset above removes. */
.sessions-fold > summary::before, .pkg-fold > summary::before {
  content: '▸'; color: var(--ink-soft); font-size: 11px; transition: transform .12s ease;
}
.sessions-fold[open] > summary::before, .pkg-fold[open] > summary::before { content: '▾'; }
.sessions-fold > summary:hover, .pkg-fold > summary:hover { color: var(--ink); }
.sessions-digest { font-size: 12.5px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
.pkg-fold { margin-top: 8px; }
.session-row {
  display: grid; gap: 8px; align-items: center; margin-bottom: 6px;
  grid-template-columns: minmax(96px, 1.1fr) 150px 110px minmax(0, 1.4fr) minmax(0, 1.2fr) 34px;
}
.session-row > input, .session-row > select { padding: 7px 8px; font-size: 13px; min-width: 0; }
.session-label { font-size: 12.5px; color: var(--ink-soft); overflow: hidden; text-overflow: ellipsis; }
/* The first appointment IS the row above. It is printed here, in the same columns as the rest, so the
   whole schedule reads as one list — a line saying only "set elsewhere" makes the reader assemble the
   plan from two places. Muted and un-editable, because the row above is where it is changed. */
.session-row.is-first > span { font-size: 12.5px; color: var(--ink-soft); }
.session-row.is-first .first-note { text-align: center; font-size: 14px; opacity: .5; }
.session-row [data-session-reset] { width: 34px; padding: 6px 0; text-align: center; }
.sessions-hint { margin: 4px 0 0; font-size: 12px; }
/* A field the clerk has set apart from the default is marked, so "what did I change?" is answerable. */
.session-row [data-touched="1"] { border-color: var(--teal); background: #f4faf8; }
@media (max-width: 900px) {
  /* Two lines rather than a squeeze: when / who, then where. The label spans both. */
  .session-row { grid-template-columns: 1fr 1fr 34px; }
  .session-row > .session-label { grid-column: 1 / 4; }
  .session-row > .s-date { grid-column: 1; }
  .session-row > .s-time { grid-column: 2; }
  .session-row > [data-session-reset] { grid-column: 3; grid-row: 2; }
  .session-row > .s-staff { grid-column: 1; }
  .session-row > .s-room { grid-column: 2 / 4; }
  .session-row.is-first { grid-template-columns: 1fr; }
}

.line-row.has-clash { outline: 2px solid #E0A33A; outline-offset: 2px; border-radius: 6px; }
.line-warn {
  margin: 6px 0 0; padding: 6px 9px; border-radius: 5px;
  background: #FDF4E3; color: #8A5A10; font-size: 12.5px; line-height: 1.45;
}

/* The filter itself. Both the therapist grid and the room grid carry the same [data-status]. */
body.hide-status-pending    .cal-ev[data-status="pending"],
body.hide-status-in_session .cal-ev[data-status="in_session"],
body.hide-status-completed  .cal-ev[data-status="completed"],
body.hide-status-cancelled  .cal-ev[data-status="cancelled"] { display: none; }
.sw { width: 11px; height: 11px; border-radius: 3px; display: inline-block; border: 1px solid rgba(0,0,0,.08); }

/*
 * ── The calendar grid ───────────────────────────────────────────────────────────────────────────────
 *
 * 🚨 `.cal-head-cell` USED `top: 60px` AND IT COVERED THE FIRST 45 MINUTES OF EVERY DAY.
 *
 * The 60px was meant to clear the sticky topbar, which would be right if the header stuck to the
 * VIEWPORT. It does not: `.cal-wrap` sets `overflow-x: auto`, and a box that scrolls on one axis is a
 * scroll container on both — so it becomes the sticky containing block, and `top: 60px` positioned the
 * opaque header 60px INSIDE the grid. Measured: wrap top 177 + 60 = 238, while the 08:00 row began at
 * 222, so three slots sat underneath a header at `z-index: 3`. The same declaration also meant the
 * header never actually stuck (it scrolled to y = -362 after 600px), leaving unlabelled columns.
 *
 * Fixed by making `.cal-wrap` scroll on BOTH axes with a bounded height, so the header sticks at
 * `top: 0` of a box that really does scroll — the behaviour a day view needs: header and time gutter
 * pinned, the day scrolling underneath.
 */
.cal-wrap {
  /* 🔑 NO max-height any more. The whole 08:00-20:00 shift is meant to be visible at once, so the grid
     sizes its rows to the viewport instead of capping its height and scrolling. `overflow: auto` stays for
     the HORIZONTAL axis (a 20-therapist outlet still scrolls sideways, driven by the arrow buttons); with
     the rows now fitting, the vertical scrollbar simply never appears. The two axes cannot be set
     independently — CSS coerces `overflow-y: visible` to `auto` when overflow-x is not visible — so this
     is one declaration on purpose. */
  overflow: auto;
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  scroll-behavior: smooth;              /* the arrow buttons glide rather than jump */
  overscroll-behavior-x: contain;       /* scrolling past the last therapist must not navigate back */
}
.cal-grid { display: grid; grid-template-columns: 62px repeat(var(--cols), minmax(168px, 1fr)); min-width: fit-content; }
.cal-gutter, .cal-col { display: flex; flex-direction: column; }
.cal-col { border-left: 1px solid var(--line); }
/* The time gutter stays put while the therapist columns scroll sideways — without this, scrolling right
   leaves rows with no time against them, which is what made the arrows necessary but not sufficient. */
.cal-gutter { position: sticky; left: 0; z-index: 4; background: var(--white); }
/*
 * 🔑 THE NAME IN FULL, AND EVERY HEADER THE SAME HEIGHT (2026-08-14, Hany).
 *
 * This was `height: 44px` + `nowrap` + `text-overflow: ellipsis`, so "Hader Ibrahem Abd El Ganey" — 26
 * characters, and not the longest on the roster — read as "Hader Ibrahem Abd…". The desk books by
 * looking down a column; a truncated name is the one thing the column header exists to show.
 *
 * 🚨 THE FIXED HEIGHT IS LOAD-BEARING, so wrapping had to keep it fixed rather than let it grow. Every
 * column is its own flex stack in one grid: if one header wraps to two lines and its neighbour does not,
 * that column's lanes start 15px lower and every appointment in it reads at the wrong time against the
 * gutter — the same class of fault as the `top: 60px` note above, and harder to see because the grid
 * still looks tidy. So: a taller fixed height that fits TWO lines, and a clamp so a freak third line is
 * hidden rather than allowed to push the lane down.
 */
.cal-head-cell {
  height: 58px; padding: 6px 10px; font-size: 13px; font-weight: 600; line-height: 1.25;
  display: flex; align-items: center; gap: 6px; overflow: hidden;
  background: #fcfbf8; border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 3;
}
/* The name wraps and clamps; the off-duty pill stays on its own line and never squeezes the name. */
.cal-head-cell .cal-name {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; overflow-wrap: anywhere; min-width: 0;
}
/* The corner cell is sticky on BOTH axes, so it must outrank the row header and the column header it
   crosses over, or the times slide under it. */
.cal-gutter .cal-head-cell { z-index: 5; }

/*
 * Small screens. The stylesheet previously had NO breakpoint below 1080px, so a phone got the desktop
 * layout: 18 nav links stacking 664px tall. Here the nav becomes one horizontally scrollable strip —
 * every link stays reachable and the bar stays one row high, which is what a phone needs from a
 * navigation bar. `-webkit-overflow-scrolling` keeps it kinetic on iOS.
 */
@media (max-width: 720px) {
  .topbar { padding: 6px 12px; gap: 8px; }
  .whoami { text-align: left; }

  /* No height cap here either: the row height clamps itself to the viewport, and on a screen too short to
     hold 48 legible rows the 11px floor wins and the grid scrolls — the honest outcome on a phone, rather
     than 10px rows nobody can tap. The topbar takes two rows at this width, so there is less to divide. */

  /* The arrow positions that used to live here were DEAD: `.cal-arrow.is-prev` below carries the same
     specificity and comes later in the file, so it won a cascade this block could never influence, and
     the computed `left` at 390px was the 900px block's 68px, never the 2px written here. Placement now
     happens in one place, against the scroller's gutter. */
}

/* ── horizontal scroll controls (replacing mouse-wheel / trackpad only) ──────────────────────────── */
/*
 * 🚨 THE ARROWS USED TO STAND ON THE GRID THEY SCROLL (2026-08-21).
 *
 * At `left/right: -14px` a 44px button keeps 30px of itself over the scroller, and the <=900px block
 * moved them fully inside (68px / 6px) to get the ‹ off the sticky time gutter — which solved the
 * gutter by standing on a therapist's lane instead. Measured at 1920 → 390: 30–38px of every leftmost
 * and rightmost visible lane, permanently, behind an opaque button with a shadow. Two costs, and the
 * second is the real one: the card underneath is hidden, and the empty slot underneath cannot be
 * clicked — on a screen whose own subtitle says "click any empty slot to book it".
 *
 * 🔑 The gutter is the fix, not the position. The scroller is padded by exactly the part that used to
 * overlap, so the button lands on the padding and its LOOK is unchanged — it still straddles the grid's
 * edge, because the edge has moved inward to meet it. Costs 60px of grid width on a desktop (4.6% of
 * 1312) and 48px on a phone, where the lane still clears its 168px minimum.
 */
.cal-scroller { position: relative; padding-inline: 30px; }
.cal-arrow {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 44px; height: 44px;              /* 44px is the minimum comfortable tap target */
  border-radius: 50%; border: 1px solid var(--line); background: var(--white);
  color: var(--ink); font-size: 18px; line-height: 1; cursor: pointer;
  display: grid; place-items: center; z-index: 6;
  box-shadow: 0 2px 10px rgba(20, 32, 46, .18);
}
.cal-arrow:hover { background: var(--sand); }
.cal-arrow:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.cal-arrow.is-prev { left: -14px; }
.cal-arrow.is-next { right: -14px; }
/* Disabled rather than removed, so the control does not move as you reach either end. */
.cal-arrow[disabled] { opacity: .35; cursor: default; box-shadow: none; }
/* Hidden entirely when the grid fits — no false affordance on a 4-therapist day. */
.cal-scroller[data-scrollable="no"] .cal-arrow { display: none; }
.cal-time {
  height: var(--rowh); font-size: 11px; color: var(--ink-soft); text-align: right;
  padding-right: 8px; font-variant-numeric: tabular-nums; line-height: var(--rowh);
}
.cal-time.is-hour { color: var(--ink); font-weight: 600; border-top: 1px solid var(--line); }
.cal-lane { position: relative; }
.cal-lane.is-away { background: repeating-linear-gradient(45deg, #faf7f2, #faf7f2 6px, #f2ece2 6px, #f2ece2 12px); }
/*
 * 🚨 THE SEPARATOR IS DRAWN ON THE TOP OF A SLOT, NOT THE BOTTOM.
 *
 * `is-hour` marks the slot that STARTS at the hour, so a border-bottom put the solid hour rule at the END of
 * that slot — 15 minutes late, at :15, which is exactly what Hany saw. Every slot now draws its own top
 * edge: the 09:00 slot carries the solid line at 09:00, and the :15/:30/:45 slots carry the dotted ones at
 * their own start. Safe because `box-sizing: border-box` is global, so the border cannot change row height
 * and push the lanes out of step with the time gutter.
 */
.cal-slot { height: var(--rowh); border-top: 1px dotted #efeae1; }
.cal-slot.is-hour { border-top: 1px solid var(--line); }

/*
 * An appointment's box. The view hands over four numbers — --top and --span in SLOTS, --col and --cols
 * for the overlap split — and the arithmetic happens here, because only the browser knows the row height
 * (computed from the viewport so a whole shift fits) and the lane width.
 *
 * 🚨 --col/--cols is what stopped two treatments at the same time being painted ON TOP of each other.
 * With `left: 3px; right: 3px` on every event, an overlap superimposed both sets of text and neither was
 * readable. A cancelled treatment keeps its time while its slot is freed for a replacement, and one room
 * holds several therapists' work, so overlaps are ordinary rather than exceptional.
 */
.cal-ev {
  position: absolute; border-radius: 7px; padding: 3px 7px;
  top: calc(var(--rowh) * var(--top));
  height: calc(var(--rowh) * var(--span) - 2px);
  left: calc(3px + (100% - 6px) * var(--col) / var(--cols));
  width: calc((100% - 6px) / var(--cols) - 2px);
  font-size: 12px; line-height: 1.25; overflow: hidden; text-decoration: none;
  color: var(--ink); border: 1px solid rgba(20,32,46,.10); display: block;
}
/* Sharing a lane costs width, so the hovered one comes forward whole rather than staying a sliver. */
.cal-ev:hover { z-index: 3; }
.cal-ev:hover { filter: brightness(.97); box-shadow: 0 2px 6px rgba(20,32,46,.12); z-index: 2; }
.cal-ev .ev-time { font-weight: 700; font-variant-numeric: tabular-nums; margin-right: 4px; }
.cal-ev .ev-guest { display: block; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cal-ev .ev-svc, .cal-ev .ev-room { display: block; color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/*
 * The other axis's assignment, sharing the time's row: the room on the therapist grid, the therapist on
 * the room grid. As its own line it was hidden on any card shorter than 47px — which is every treatment
 * under an hour — while the time row sat five characters wide and otherwise empty.
 */
/*
 * ⚠️ NOT A FLEX ROW. Making it one to right-align the name grew the row from 11px to 13px — a flex
 * container takes the height of its tallest item — and the card's content then overflowed its box on 9
 * of 14 cards, quietly cutting off the treatment name to make room for the therapist's. It is one line
 * of text, so it stays one line of text: the name follows the time after a middot and shares the line's
 * own ellipsis when the lane is narrow.
 */
/* Tighter than the lines below it by design: as an inline span its height came from the line box's
   strut, and blockifying it added a pixel — which is all it takes for a 40px card to clip its third
   line at a 900px window, where the grid sizes its rows to fit the day. */
.cal-ev .ev-time {
  display: block; line-height: 1.1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cal-ev .ev-with { font-style: normal; font-weight: 400; color: var(--ink-soft); }
.cal-ev .ev-with::before { content: '· '; }
/* The room's type qualifies its name; at a narrow lane the name comes first and the type gives way. */
.cal-head-cell .head-type { font-size: 11px; }
@media (max-width: 900px) { .cal-head-cell .head-type { display: none; } }
.ev-pending   { background: #fdf3dd; }
.ev-session   { background: #dff1e6; }
.ev-done      { background: #e8eef5; }
.ev-cancelled { background: #f4f1ec; text-decoration: line-through; opacity: .7; }
.vip { color: var(--gold); font-style: normal; }
.nopost { color: var(--danger); font-style: normal; }

/* ---------- booking form ---------- */
.line-row { border: 1px solid var(--line); border-radius: 10px; padding: 10px; margin-bottom: 10px; background: #fcfbf8; }
/*
 * ONE TREATMENT, ONE LINE (Hany, 2026-08-17: "some services go in 2 lines … sometimes the delete
 * button only").
 *
 * 🚨 THE BUG WAS `flex-wrap` PLUS `min-width: 150px`. Every control declared a floor and the row was
 * allowed to wrap, so as soon as the six of them exceeded the card the last ones dropped to a second
 * line — the room select and the ×, or on a slightly narrower card the × on its own. It varied ROW BY
 * ROW because a flex item sizes to its content and the treatment names differ in length, which is why
 * it read as random. Adding the quantity box pushed ordinary laptop widths over the edge.
 *
 * A GRID FIXES IT BY CONSTRUCTION rather than by tuning: the columns are declared up front, the
 * flexible ones are `minmax(0, Nfr)` so a long option truncates instead of shoving its neighbours out,
 * and there is nowhere to wrap to. Each row type has its own template because they hold different
 * controls — a package has no quantity, and retail has no time, therapist or room.
 *
 * 🔑 `min-width: 0` on the controls is the load-bearing line. A grid item's default `min-width: auto`
 * refuses to shrink below its content, which is the same overflow in a different costume.
 */
/*
 * ⚠️ ONE TEMPLATE FOR ALL THREE ROW TYPES (2026-08-18 audit). They each had their own column list, so a
 * treatment, a package and a product on the same booking lined up with nothing: the time sat at three
 * different x positions down a single card, which reads as four unrelated forms rather than one list.
 * The columns are now shared and each type places what it has — a package spans the quantity column it
 * does not use, retail spans the time/therapist/room columns it does not use.
 */
.line-main {
  display: grid; gap: 8px; align-items: center;
  grid-template-columns:
    minmax(0, 2fr)      /* what */
    58px                /* how many */
    118px               /* when */
    minmax(0, 1fr)      /* who */
    minmax(0, 1fr)      /* where */
    72px                /* how much */
    32px;               /* remove */
}
/*
 * 🚨 PLACED BY FIELD, NOT BY WRAPPER CLASS (2026-08-21 — the three-line row, second time round).
 *
 * This rule used to read `.line-main > .ss, .line-main > .svc { grid-column: 1 }`, which was true only
 * while the treatment chooser was the only select on the row long enough to be enhanced.
 * `searchable-select.js` wraps EVERY select with 8 or more options in `.ss`, and this outlet's catalogue
 * grew to 27 therapists and 14 rooms — so all three controls became `.ss`, all three were pinned to
 * column 1, and the first row stacked into three lines. Not one line of code changed: the DATA crossed
 * the threshold. Below an 800px container it was worse than untidy — the narrow rule put all three in
 * `grid-column: 1 / 5; grid-row: 1`, so the therapist and the room rendered exactly on top of the
 * treatment chooser and could not be reached.
 *
 * Only the FIRST row showed it, which is what made it read as random: `scan()` runs once on
 * DOMContentLoaded, and rows added afterwards are cloned from `<template>` and never enhanced.
 *
 * The 2026-08-18 audit had already learned this lesson for the narrow layout and keyed those rules to
 * `[name$=…]` — and they broke here for the very same reason, because a wrapped select is no longer a
 * direct child, and the wrapper carries no name. So `searchable-select.js` now stamps the wrapper with
 * `data-field="<the select's name>"`, and every rule below names its FIELD and matches it bare OR
 * wrapped. Wrapper-proof and threshold-proof: the package or product list crossing 8 options changes
 * nothing now.
 *
 * 🔑 `min-width: 0` on every item stays load-bearing — a grid item's default `min-width: auto` refuses
 * to shrink below its content, which is the row overflowing in a different costume.
 */
.line-main > * { min-width: 0; }
.line-row[data-type="service"] .line-main > .svc,
.line-row[data-type="service"] .line-main > [data-field$="[sellable_id]"] { grid-column: 1; }
.line-row[data-type="service"] .line-main > .qty  { grid-column: 2; }
.line-row[data-type="service"] .line-main > .time { grid-column: 3; }
.line-row[data-type="package"] .line-main > .pkg,
.line-row[data-type="package"] .line-main > [data-field$="[sellable_id]"] { grid-column: 1 / 3; }
.line-row[data-type="package"] .line-main > .time { grid-column: 3; }
.line-row[data-type="service"] .line-main > [name$="[staff_id]"],
.line-row[data-type="service"] .line-main > [data-field$="[staff_id]"],
.line-row[data-type="package"] .line-main > [name$="[staff_id]"],
.line-row[data-type="package"] .line-main > [data-field$="[staff_id]"] { grid-column: 4; }
.line-row[data-type="service"] .line-main > [name$="[resource_id]"],
.line-row[data-type="service"] .line-main > [data-field$="[resource_id]"],
.line-row[data-type="package"] .line-main > [name$="[resource_id]"],
.line-row[data-type="package"] .line-main > [data-field$="[resource_id]"] { grid-column: 5; }
.line-row[data-type="retail"] .line-main > select,
.line-row[data-type="retail"] .line-main > [data-field$="[sellable_id]"] { grid-column: 1; }
.line-row[data-type="retail"] .line-main > .qty { grid-column: 2; }
.line-row[data-type="retail"] .line-main > .muted { grid-column: 3 / 6; }
.line-main > .line-money { grid-column: 6; }
.line-main > [data-remove] { grid-column: 7; }
.line-main select, .line-main input {
  padding: 8px 10px; font: inherit; font-size: 14px; border: 1px solid var(--line);
  border-radius: 8px; background: var(--white); color: var(--ink);
  min-width: 0; width: 100%;
}
.line-main .qty { width: 100%; text-align: center; padding-left: 4px; padding-right: 4px; }
.line-main [data-remove] { width: 32px; padding: 8px 0; text-align: center; }
.line-main .muted { font-size: 12px; line-height: 1.2; }


/*
 * 🚨 THE ROW ANSWERS TO THE CARD IT IS IN, NOT TO THE WINDOW (2026-08-18 audit).
 *
 * Keying the stack to the viewport measured the wrong thing. Between 1081px and 1365px the page is still
 * in TWO columns, so the treatments card is only 654–834px wide while the window looks roomy — and the
 * row stayed on one line and truncated the two fields a clerk most needs to read: the therapist showed
 * "abber el" and the room "Couples". At 1100px the therapist select was 64px. 1280px is an ordinary
 * laptop, so this was the common case, not an edge.
 *
 * `#lines` becomes a query container and the row stacks when THAT is too narrow, whatever the window is
 * doing. The viewport copy below stays as the fallback for browsers without container queries.
 */
#lines { container-type: inline-size; }
@container (max-width: 800px) {
  /*
   * ⚠️ EVEN COLUMNS, because this layout is no longer only a phone's. It began as a 480px rule where a
   * single `1fr` column was small; used at an 834px card it made the QUANTITY box 626px wide with a "3"
   * floating in the middle of it, while the room select took the rest. Four equal columns plus the
   * remove button keep the pairs balanced at every size this now covers: what / how many · when · how
   * much / with whom · where.
   */
  .line-row[data-type="service"] .line-main,
  .line-row[data-type="package"] .line-main {
    grid-template-columns: repeat(4, minmax(0, 1fr)) 32px;
  }
  .line-row[data-type="service"] .line-main > .svc,
  .line-row[data-type="service"] .line-main > [data-field$="[sellable_id]"],
  .line-row[data-type="package"] .line-main > .pkg,
  .line-row[data-type="package"] .line-main > [data-field$="[sellable_id]"] { grid-column: 1 / 5; grid-row: 1; }
  .line-row[data-type="service"] .line-main > [data-remove],
  .line-row[data-type="package"] .line-main > [data-remove] { grid-column: 5; grid-row: 1; }
  .line-row[data-type="service"] .line-main > .qty { grid-column: 1; grid-row: 2; }
  .line-row[data-type="service"] .line-main > .time { grid-column: 2 / 4; grid-row: 2; }
  .line-row[data-type="package"] .line-main > .qty-gap { display: none; }
  .line-row[data-type="package"] .line-main > .time { grid-column: 1 / 4; grid-row: 2; }
  .line-row[data-type="service"] .line-main > .line-money,
  .line-row[data-type="package"] .line-main > .line-money { grid-column: 4 / 6; grid-row: 2; }
  .line-row[data-type="service"] .line-main > [name$="[staff_id]"],
  .line-row[data-type="service"] .line-main > [data-field$="[staff_id]"],
  .line-row[data-type="package"] .line-main > [name$="[staff_id]"],
  .line-row[data-type="package"] .line-main > [data-field$="[staff_id]"] { grid-column: 1 / 3; grid-row: 3; }
  .line-row[data-type="service"] .line-main > [name$="[resource_id]"],
  .line-row[data-type="service"] .line-main > [data-field$="[resource_id]"],
  .line-row[data-type="package"] .line-main > [name$="[resource_id]"],
  .line-row[data-type="package"] .line-main > [data-field$="[resource_id]"] { grid-column: 3 / 6; grid-row: 3; }

  /* Retail is three controls and a note; the note wraps under them rather than squeezing the product. */
  .line-row[data-type="retail"] .line-main { grid-template-columns: minmax(0, 1fr) 58px 32px; }
  .line-row[data-type="retail"] .line-main > select,
  .line-row[data-type="retail"] .line-main > [data-field$="[sellable_id]"] { grid-column: 1; grid-row: 1; }
  .line-row[data-type="retail"] .line-main > .qty { grid-column: 2; grid-row: 1; }
  .line-row[data-type="retail"] .line-main > [data-remove] { grid-column: 3; grid-row: 1; }
  .line-row[data-type="retail"] .line-main > .muted { grid-column: 1 / 3; grid-row: 2; }
  .line-row[data-type="retail"] .line-main > .line-money { grid-column: 3; grid-row: 2; }
}

/*
 * A phone cannot hold seven controls side by side at any font size worth reading. Below this it breaks
 * into rows — what, then how many and when and how much, then with whom and where.
 *
 * ⚠️ THE BREAKPOINT WAS 560px AND THAT WAS TOO LATE, measured on the 2026-08-18 audit: at 820px — a
 * tablet, which is what the desk actually uses — the row still fitted on one line by truncating the two
 * things a clerk most needs to read, so the therapist showed "abber el" and the room "Couples". A name
 * cut in half is worse than a second line: the room called "Couples suite" and one called "Couples
 * treatment room" become the same word. Stacking now starts where the columns stop being legible.
 */
@media (max-width: 900px) {
  /*
   * ⚠️ MEASURED, NOT ASSUMED: the first attempt kept the treatment beside the quantity and the time,
   * which left it 52px wide on a 375px screen — the select read "Sh". The treatment is the widest thing
   * on the row and gets a row of its own; the small controls share the ones below it.
   *
   * Placed explicitly, never by auto-flow, which would drop a select into the column the × lives in.
   * `.svc`/`.pkg` is the treatment; among the selects, the 2nd and 3rd are the therapist and the room.
   */
  /*
   * 🚨 PLACED BY NAME, NOT BY POSITION. These rules used to say `.svc` and `select:nth-of-type(2)`, and
   * both quietly stopped matching when the treatment chooser gained its `.ss` searchable wrapper: the
   * treatment was no longer a direct child, so among the remaining direct selects the "2nd" became the
   * ROOM and the therapist fell through to auto-placement — 58px wide, reading "ab" on a phone. A
   * layout keyed to counting siblings breaks the next time the markup around it changes; keyed to the
   * field's own name it does not.
   */
  /*
   * ⚠️ EVEN COLUMNS, because this layout is no longer only a phone's. It began as a 480px rule where a
   * single `1fr` column was small; used at an 834px card it made the QUANTITY box 626px wide with a "3"
   * floating in the middle of it, while the room select took the rest. Four equal columns plus the
   * remove button keep the pairs balanced at every size this now covers: what / how many · when · how
   * much / with whom · where.
   */
  .line-row[data-type="service"] .line-main,
  .line-row[data-type="package"] .line-main {
    grid-template-columns: repeat(4, minmax(0, 1fr)) 32px;
  }
  .line-row[data-type="service"] .line-main > .svc,
  .line-row[data-type="service"] .line-main > [data-field$="[sellable_id]"],
  .line-row[data-type="package"] .line-main > .pkg,
  .line-row[data-type="package"] .line-main > [data-field$="[sellable_id]"] { grid-column: 1 / 5; grid-row: 1; }
  .line-row[data-type="service"] .line-main > [data-remove],
  .line-row[data-type="package"] .line-main > [data-remove] { grid-column: 5; grid-row: 1; }
  .line-row[data-type="service"] .line-main > .qty { grid-column: 1; grid-row: 2; }
  .line-row[data-type="service"] .line-main > .time { grid-column: 2 / 4; grid-row: 2; }
  .line-row[data-type="package"] .line-main > .qty-gap { display: none; }
  .line-row[data-type="package"] .line-main > .time { grid-column: 1 / 4; grid-row: 2; }
  .line-row[data-type="service"] .line-main > .line-money,
  .line-row[data-type="package"] .line-main > .line-money { grid-column: 4 / 6; grid-row: 2; }
  .line-row[data-type="service"] .line-main > [name$="[staff_id]"],
  .line-row[data-type="service"] .line-main > [data-field$="[staff_id]"],
  .line-row[data-type="package"] .line-main > [name$="[staff_id]"],
  .line-row[data-type="package"] .line-main > [data-field$="[staff_id]"] { grid-column: 1 / 3; grid-row: 3; }
  .line-row[data-type="service"] .line-main > [name$="[resource_id]"],
  .line-row[data-type="service"] .line-main > [data-field$="[resource_id]"],
  .line-row[data-type="package"] .line-main > [name$="[resource_id]"],
  .line-row[data-type="package"] .line-main > [data-field$="[resource_id]"] { grid-column: 3 / 6; grid-row: 3; }

  /* Retail is three controls and a note; the note wraps under them rather than squeezing the product. */
  .line-row[data-type="retail"] .line-main { grid-template-columns: minmax(0, 1fr) 58px 32px; }
  .line-row[data-type="retail"] .line-main > select,
  .line-row[data-type="retail"] .line-main > [data-field$="[sellable_id]"] { grid-column: 1; grid-row: 1; }
  .line-row[data-type="retail"] .line-main > .qty { grid-column: 2; grid-row: 1; }
  .line-row[data-type="retail"] .line-main > [data-remove] { grid-column: 3; grid-row: 1; }
  .line-row[data-type="retail"] .line-main > .muted { grid-column: 1 / 3; grid-row: 2; }
  .line-row[data-type="retail"] .line-main > .line-money { grid-column: 3; grid-row: 2; }
}
/*
 * 🚨 THE MONEY. The card is headed "prices in EUR" and then showed none: the only figure anywhere was
 * buried in each dropdown's option text, so a booking of three massages, a package and a bottle of oil
 * left the desk with no idea what the guest owed — on the screen where the guest is standing there
 * asking. Each line states its own amount and the card totals them.
 */
.line-money {
  text-align: right; font-size: 13px; font-variant-numeric: tabular-nums; color: var(--ink);
  white-space: nowrap;
}
.line-money.is-none { color: var(--ink-soft); }
.lines-total {
  display: flex; align-items: baseline; justify-content: flex-end; gap: 12px;
  margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line);
}
.lines-total .label { font-size: 13px; color: var(--ink-soft); }
.lines-total .amount { font-size: 20px; font-weight: 600; font-variant-numeric: tabular-nums; }
.lines-total .ccy { font-size: 13px; color: var(--ink-soft); }
.total-note { margin: 6px 0 0; text-align: right; font-size: 12px; color: var(--ink-soft); }
/* Divides the guest from the booking inside one card: the fields below it are about the BOOKING —
   its date, how it was taken, who sold it — not about the person. */
.sub-rule {
  margin: 20px 0 12px; padding-top: 14px; border-top: 1px solid var(--line);
  font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
  color: var(--ink-soft);
}
/* Says why the Check in button is not there. Amber rather than red: nothing is wrong, something is
   simply not decided yet. Kept to one short line — as two it widened the actions column until the
   column was clipped off the card. */
.needs-assign {
  display: inline-block; font-size: 12px; line-height: 1.3; color: #8a6412; white-space: nowrap;
}
/*
 * 🚨 A BOOKING'S LINE TABLE IS PROSE AND FIGURES TOGETHER. `th, td { white-space: nowrap }` suits money
 * columns; applied to a treatment name, a therapist and a room it made a seven-column table wider than
 * its card, so the ACTIONS column — the one with Check in and More on it — was cut off the right edge
 * (55px at 1600, 135px at 1280). The three prose columns wrap; the figures do not.
 */
.line-table td:nth-child(1),
.line-table td:nth-child(3),
.line-table td:nth-child(4) { white-space: normal; }
.line-table td:nth-child(3), .line-table td:nth-child(4) { min-width: 92px; }
/* The treatment name is the column a reader scans; at 145px "Face & Scalp Massage 50 Min (2 of 8)"
   broke over three lines and made every row three lines tall. */
.line-table td:nth-child(1), .line-table th:nth-child(1) { min-width: 200px; }
.line-table th, .line-table td { padding-left: 12px; padding-right: 12px; }
/*
 * On a phone the table is still wider than the screen once a treatment name, a time, a therapist and a
 * status are on it — so the ACTION stays put and the detail scrolls under it. Hiding the action column
 * would be worse than a scrollbar: checking a guest in is the reason this screen is open.
 */
@media (max-width: 700px) {
  /* 🚨 `position: sticky` DOES NOTHING on a cell of a `border-collapse: collapse` table in Chrome — the
     rule applies, the cell does not stick, and the measurement says "hidden at rest" while the CSS looks
     right. Separated borders, with the row rule redrawn as a bottom border on the cells themselves. */
  .line-table { border-collapse: separate; border-spacing: 0; }
  .line-table th, .line-table td { border-bottom: 1px solid var(--line); }
  .line-table th:last-child, .line-table td:last-child {
    position: sticky; right: 0; background: var(--white);
    box-shadow: -7px 0 7px -7px rgba(31, 27, 22, .25);
  }
  .line-table thead th:last-child { background: var(--sand); }
}

/* The bill's own figures, held to a readable measure and parked on the right where the money column
   is. Full width put a label and its number ~900px apart once the card was widened. */
.money-summary { max-width: 460px; margin-left: auto; }
.money-summary .kv { width: 100%; }
/* The discount form is three short questions; stretched across a wide card the "Why" box alone was
   875px, which reads as an essay field for what is usually two words. */
.discount-form { max-width: 620px; }

/*
 * 🚨 THE TREATMENT NAME IS WHAT MAKES A BILL TOO WIDE, not the money columns. `th, td { white-space:
 * nowrap }` is right for a row of figures and wrong for "4 Hands Synchronized Massage 60 Min (Double)":
 * on a phone that single cell is wider than the screen, so the NET column — the figure the guest is
 * being asked to pay — was pushed 257px off the edge with the detail columns already folded away. The
 * one column that is prose is allowed to behave like prose.
 */
.bill-table td:first-child { white-space: normal; min-width: 140px; }

/*
 * 🚨 THE ELEVENTH COLUMN. Adding Room pushed the invoice list 79px past its card, and the column that
 * goes over the edge is always the last one — here the Print button. The two prose columns (outlet and
 * guest) wrap instead; every other column is a figure, a serial or a pill and stays on one line.
 */
/* ⚠️ `white-space: normal` alone changed nothing: inside a `.table-scroll` (overflow-x: auto) the table
   is laid out at its PREFERRED width, so a wrappable column keeps its longest line rather than being
   squeezed. Given a width to aim at, it wraps and the table comes back inside its card. */
.inv-table td:nth-child(3), .inv-table th:nth-child(3) { white-space: normal; width: 130px; }
.inv-table td:nth-child(4), .inv-table th:nth-child(4) { white-space: normal; width: 140px; }
.inv-table th, .inv-table td { padding-left: 12px; padding-right: 12px; }
/*
 * Below a laptop, eleven columns of money cannot fit whatever wraps — so nothing is thrown away and the
 * PRINT action stays put while the figures scroll under it. Same lesson as the booking screen: sticky is
 * inert on a `border-collapse: collapse` table in Chrome, so the borders separate here and the row rule
 * is redrawn on the cells.
 */
@media (max-width: 1279px) {
  .inv-table { border-collapse: separate; border-spacing: 0; }
  .inv-table th, .inv-table td { border-bottom: 1px solid var(--line); }
  .inv-table th:last-child, .inv-table td:last-child {
    position: sticky; right: 0; background: var(--white);
    box-shadow: -7px 0 7px -7px rgba(31, 27, 22, .25);
  }
  .inv-table thead th:last-child { background: var(--sand); }
}
/* On a phone the PAID column goes: Net and Outstanding are the two that decide anything, and what was
   paid is on the bill itself one tap away. */
@media (max-width: 900px) {
  .inv-table th:nth-child(7), .inv-table td:nth-child(7) { display: none; }
}

/*
 * ── Report tables (2026-08-18 audit) ──────────────────────────────────────────────────────────────
 * 🚨 THE LAST COLUMN WAS GOING OVER THE EDGE. Revenue lost "Refunds" by 13px at 1280 — an ordinary
 * laptop — and by 525px at 768; Cash lost "Closed by", Tenders "Movements", Mix "Avg price". A figure
 * parked past the edge of a card is a figure nobody reads, and on a REPORT that is the whole point of
 * the screen.
 *
 * Every report marks its numeric cells `.num`, so the prose ones — dates, outlets, names, who closed a
 * drawer — can wrap while the figures stay on one line. Tighter gutters than the app default, because a
 * report is a dense grid by nature.
 */
.rep-table th, .rep-table td { padding-left: 10px; padding-right: 10px; }
.rep-table th:not(.num), .rep-table td:not(.num) { white-space: normal; max-width: 190px; }

/*
 * 🔑 AND WHEN A TABLE STILL HAS TO SCROLL, IT SAYS SO. A wide numeric report cannot always fit, and the
 * honest answer is a scrollbar — but a bare cut edge reads as "that is all there is". These shadows sit
 * on whichever side has more content and disappear when you reach it: `background-attachment: local`
 * scrolls the white masks with the content while the shadows stay put, so no script is involved.
 */
.table-scroll {
  background:
    linear-gradient(to right, var(--white) 30%, rgba(255, 255, 255, 0)) left center,
    linear-gradient(to left, var(--white) 30%, rgba(255, 255, 255, 0)) right center,
    radial-gradient(farthest-side at 0 50%, rgba(31, 27, 22, .13), rgba(31, 27, 22, 0)) left center,
    radial-gradient(farthest-side at 100% 50%, rgba(31, 27, 22, .13), rgba(31, 27, 22, 0)) right center;
  background-repeat: no-repeat;
  background-size: 34px 100%, 34px 100%, 12px 100%, 12px 100%;
  background-attachment: local, local, scroll, scroll;
}

/* A bill's per-line discount and tax are detail; the NET is the answer. Below this width the detail
   columns fold away so the answer stays on screen instead of behind a sideways scroll. Both figures are
   still stated for the whole bill in the summary under the table. */
@media (max-width: 700px) { .col-detail { display: none; } }
.row-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.line-actions { display: flex; gap: 6px; justify-content: flex-end; flex-wrap: wrap; }
.lookup { margin-top: 10px; }
.cand { display: flex; gap: 9px; align-items: flex-start; padding: 9px; border: 1px solid var(--line);
        border-radius: 8px; margin-bottom: 7px; background: var(--white); cursor: pointer; font-size: 13px; }
.cand input { margin-top: 3px; accent-color: var(--teal); }
details.manual { margin-top: 14px; }

table.kv { font-size: 14px; }
table.kv th { width: 44%; background: none; text-transform: none; letter-spacing: 0; font-size: 13px;
              color: var(--ink-soft); font-weight: 500; padding: 7px 0; border-bottom: 1px solid var(--line); }
table.kv td { padding: 7px 0; border-bottom: 1px solid var(--line); }

/* An invoice's bill has six numeric columns; the default narrow sidebar clipped them. */
.two-col--wide { grid-template-columns: minmax(420px, 1fr) minmax(360px, 1fr); }
/*
 * The mirror of `.two-col`: the CONTENT is on the left and the narrow aside on the right. `.two-col`
 * fixes its FIRST column, which is right when the form leads and the table follows (the cash drawer) and
 * exactly wrong when the table leads (purchasing) — there it squeezed the orders table into 380px and
 * clipped its Status column. One class per shape beats remembering which way round the page is.
 */
.two-col--aside { grid-template-columns: minmax(0, 1fr) minmax(300px, 380px); }
@media (max-width: 1080px) { .two-col--aside { grid-template-columns: 1fr; } }

/*
 * ── The staff edit page (2026-08-14) ────────────────────────────────────────────────────────────────
 * Four small classes, added because the record moved out of a `<details>` popover in a table cell and
 * on to a page of its own. Everything else it uses — .card, .field, .check, .two-col, .crumb — already
 * existed and is reused rather than re-invented.
 */
/* Two short fields side by side; collapses rather than squeezing on a narrow screen. */
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 0 14px; }
@media (max-width: 640px) { .field-row { grid-template-columns: 1fr; } }
/* Cards stacked in one column of the two-column grid, matching the grid's own gap. */
.stack { display: flex; flex-direction: column; gap: 18px; }
/* Outlets and skills read as a scannable block, not a single tall ribbon of checkboxes. */
.checkgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 2px 14px; }
/* The actions repeated at the foot of a long form, so a full skill list need not be scrolled back up. */
.form-foot { margin-top: 18px; justify-content: flex-end; }
/* The permission grid on a role page, separated from the name/members pair above it. */
.role-perms { margin-top: 22px; }
/* An amber notice: a condition that will refuse a save, stated before it is hit. */
.notice-warn { background: #FDF4E3; border-color: #EBD9B4; color: #8A5A10; }
@media (max-width: 1080px) { .two-col--wide { grid-template-columns: 1fr; } }

/* ---- clickable calendar slots -------------------------------------------------
 * An empty slot is where a booking starts, so it behaves like a control: the whole
 * cell is the target, and hovering shows a + at the time you are about to book.
 * Kept subtle because 12 hours x 8 therapists is ~380 of these on screen at once. */
a.cal-slot { display: block; text-decoration: none; position: relative; }
a.cal-slot.is-open:hover {
  background: rgba(15, 138, 134, .10);
  box-shadow: inset 0 0 0 1px rgba(15, 138, 134, .45);
}
a.cal-slot.is-open:hover::after {
  content: "+"; position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  font-size: 13px; line-height: 1; color: var(--teal-dark); font-weight: 700;
}
a.cal-slot.is-open:focus-visible {
  outline: 2px solid var(--teal); outline-offset: -2px;
}
.cal-lane.is-away a.cal-slot.is-open:hover {
  background: rgba(180, 67, 58, .10);
  box-shadow: inset 0 0 0 1px rgba(180, 67, 58, .40);
}
.cal-lane.is-away a.cal-slot.is-open:hover::after { content: "off duty"; font-size: 9px; font-weight: 600; color: var(--danger); }
.hint-click { color: var(--teal-dark); }

/* ---- what a package contains -------------------------------------------------- */
.pkg-contents { margin: 8px 0 2px 0; }
.pkg-list { margin: 0 0 6px 0; padding-left: 18px; font-size: 13.5px; }
.pkg-list li { margin-bottom: 2px; }
.btn-gold { border-color: var(--gold); color: #7a5f1a; background: #fdf8ea; }
.btn-gold:hover { background: #f9f0d8; }
.ent { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; flex-wrap: wrap; }

/* A warning is neither a success notice nor an error: the action succeeded and something about it
 * still needs saying. Gold, because that is this palette's "attend to this" colour. */
.warn {
  background: #fdf8ea; border: 1px solid var(--gold); border-left: 3px solid var(--gold);
  color: #6f5416; padding: 11px 14px; border-radius: var(--radius); margin: 0 0 16px; font-size: 14.5px;
}

/* ---- audit log ---------------------------------------------------------------- */
.filter-row { display: flex; flex-wrap: wrap; gap: 12px 16px; align-items: flex-end; }
.filter-row .field { margin: 0; min-width: 140px; }
.filter-row .field-actions { display: flex; gap: 8px; }
.kv tr.is-changed th, .kv tr.is-changed td { background: #fdf8ea; }
.kv tr.is-changed td { font-weight: 600; }

/* ---- user & role admin -------------------------------------------------------- */
.check-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); gap: 4px 18px; }
.check-grid .check { margin: 0; }
.group-row { background: var(--sand); font-weight: 700; font-size: 12px; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-soft); }
.head-actions { display: flex; gap: 8px; align-items: center; }

/* ---- folio posting queue ------------------------------------------------------- */
.band { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 1px;
        background: var(--line); border: 1px solid var(--line); margin: 0 0 20px; }
.band .cell { background: var(--white); padding: 12px 14px; text-decoration: none; color: var(--ink);
              border-top: 3px solid transparent; display: block; }
.band .cell:hover { background: var(--sand); }
.band .cell.is-bad { border-top-color: var(--danger); }
.band .cell.is-bad b { color: var(--danger); }
.band .cell b { display: block; font-size: 24px; font-weight: 800; line-height: 1.1;
                font-variant-numeric: tabular-nums; }
.band .cell span { font-size: 10.5px; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-soft); }
.band .cell em { display: block; font-size: 11.5px; font-style: normal; }

/* ─────────────────────────────────────────────────────────────────────────────────────────────────────
 * LOGIN — split-screen, matching the legacy spa.sunrise-resorts.com sign-in design (2026-08-12).
 *
 * Deliberate differences from the legacy page, all of them fixes rather than drift:
 *  - No Google Fonts request. The legacy pulls Inter from fonts.googleapis.com; this app has no build
 *    step and no CDN elsewhere, and a webfont on the critical path would be the only external request
 *    on the whole login page. The existing system stack is used instead.
 *  - Icons are inline SVG, not an icon font, for the same reason.
 *  - The teal is #0f766e taken from the legacy button. White on it measures 5.50:1, which passes
 *    WCAG AA for normal text — the app's own lighter --teal measures 4.20:1 and fails, so matching the
 *    legacy here also closes an accessibility finding.
 *  - The `.lg-` prefix keeps this entirely separate from the older `.login-card` rules above, so the
 *    two cannot interfere while both exist.
 */
.lg-shell { min-height: 100vh; display: grid; grid-template-columns: 54% 46%; }
.lg-brandside {
  position: relative; overflow: hidden; padding: 40px 56px;
  display: flex; flex-direction: column; color: #eef7f5;
  background:
    radial-gradient(1200px 400px at 80% -10%, rgba(204, 251, 241, .25), transparent 60%),
    radial-gradient(600px 600px at 0% 110%, rgba(184, 145, 47, .25), transparent 60%),
    linear-gradient(135deg, #134e4a 0%, #0b3e3a 55%, #0f172a 100%);
}
.lg-lock { display: flex; align-items: center; gap: 12px; }
.lg-lock-mark {
  width: 52px; height: 52px; border-radius: 14px; flex: none; background: var(--white);
  display: grid; place-items: center; box-shadow: 0 6px 20px rgba(0, 0, 0, .22);
}
.lg-lock-mark img { width: 34px; height: 34px; object-fit: contain; display: block; }
.lg-lock-name { display: block; font-weight: 700; letter-spacing: .10em; text-transform: uppercase; font-size: 15px; }
.lg-lock-sub { display: block; font-size: 12.5px; color: rgba(238, 247, 245, .72); margin-top: 2px; }
.lg-pitch { margin: auto 0; }
/* No max-width here: `ch` resolves against the 15px body font, not the 46px headline, which forced
   "Serenity, scheduled." onto two lines. The paragraph carries its own 46ch measure instead. */
.lg-pitch h1 { font-size: clamp(30px, 3.6vw, 46px); line-height: 1.08; letter-spacing: -.025em; margin: 0 0 16px; color: #fff; }
.lg-pitch h1 em { font-style: italic; font-weight: 500; color: #f5efe0; }
.lg-pitch p { margin: 0; font-size: 15.5px; line-height: 1.6; color: rgba(238, 247, 245, .80); max-width: 46ch; }
.lg-chips { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 22px; }
.lg-chip {
  display: inline-flex; align-items: center; gap: 8px; padding: 9px 15px; border-radius: 999px;
  font-size: 13px; color: rgba(238, 247, 245, .92);
  border: 1px solid rgba(238, 247, 245, .22); background: rgba(255, 255, 255, .06);
}
.lg-chip svg { width: 15px; height: 15px; flex: none; opacity: .85; }
.lg-copy { font-size: 11.5px; letter-spacing: .08em; text-transform: uppercase; color: rgba(238, 247, 245, .55); }

.lg-formside { background: var(--sand); display: grid; place-items: center; padding: 40px 32px; }
.lg-form { width: 100%; max-width: 380px; }
.lg-form h2 { margin: 0 0 4px; font-size: 30px; letter-spacing: -.025em; }
.lg-form .lg-lede { margin: 0 0 26px; color: var(--ink-soft); font-size: 14.5px; }
.lg-form label.lg-label {
  display: block; font-size: 11px; font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--ink-soft); margin: 0 0 7px;
}
.lg-input { position: relative; margin-bottom: 16px; }
.lg-input > svg.lg-lead { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); width: 17px; height: 17px; color: var(--ink-soft); pointer-events: none; }
/* UNIFIED 2026-08-13 on Hany's instruction. These now inherit the canonical control design — padding,
   font, border, 8px radius, cream ground, focus ring — from the rule at the top of this file. Only what is
   genuinely specific to a login field is left here.

   The block that used to sit here hard-coded `height: 52px`, a white ground, a 10px radius and its own teal
   focus shadow (#14b8a6, which is not even a brand token). That is why the login screen and the
   change-password popup read as a different application from the rest of the console. */
.lg-input input { width: 100%; }
/* Room for the leading icon, applied ONLY where there is one: the change-password dialog has no icons, and
   a sibling selector keeps that automatic rather than needing a second class in the markup. */
.lg-input > svg.lg-lead ~ input { padding-left: 38px; }
/* Room for the reveal button. On the login page that is the page's own .lg-reveal (marked data-pw-reveal so
   the global partial leaves it alone); in the dialog it is the button the partial injects. */
.lg-input input[type=password] { padding-right: 42px; }
/* 34px, not 40px: the field is no longer a fixed 52px tall, so a 40px button would fill it edge to edge. */
.lg-reveal {
  position: absolute; right: 5px; top: 50%; transform: translateY(-50%);
  width: 34px; height: 34px; border: 0; background: none; cursor: pointer; color: var(--ink-soft);
  display: grid; place-items: center; border-radius: 8px; padding: 0;
}
.lg-reveal:hover { background: var(--sand); color: var(--ink); }
.lg-reveal:focus-visible { outline: 2px solid #0f766e; outline-offset: 1px; }
.lg-reveal svg { width: 18px; height: 18px; }
.lg-err { display: block; margin: -10px 0 14px; font-size: 12.5px; color: var(--danger); font-weight: 600; }
.lg-alert {
  background: #fdecea; border: 1px solid #f3c9c4; color: #8f2f27;
  border-radius: 10px; padding: 11px 14px; font-size: 13.5px; margin-bottom: 18px;
}
.lg-remember { display: flex; align-items: center; gap: 8px; font-size: 13.5px; color: var(--ink-soft); margin: 2px 0 18px; }
.lg-remember input { width: 16px; height: 16px; accent-color: #0f766e; }
.lg-submit {
  width: 100%; min-height: 52px; border: 0; border-radius: 10px; cursor: pointer;
  background: #0f766e; color: #fff; font-size: 15.5px; font-weight: 700;
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  box-shadow: 0 8px 20px rgba(15, 118, 110, .22);
}
.lg-submit:hover { background: #0b6b68; }
.lg-submit:focus-visible { outline: 3px solid rgba(15, 118, 110, .45); outline-offset: 2px; }
.lg-submit svg { width: 17px; height: 17px; }

/* One column below 900px: the brand panel becomes a compact header so the form is reachable without
   scrolling past a full-height gradient on a phone. */
@media (max-width: 900px) {
  /* `auto 1fr`: the brand panel takes exactly its content height and the form panel takes the rest.
     Without this the two rows split the viewport evenly and left ~180px of empty gradient under the
     copyright on a phone, pushing the Sign in button further down for no reason. */
  .lg-shell { grid-template-columns: 1fr; grid-template-rows: auto 1fr; min-height: 100dvh; }
  .lg-brandside { padding: 22px 22px 26px; }
  .lg-pitch { margin: 20px 0 0; }
  .lg-pitch h1 { font-size: 27px; }
    /* The chips and the paragraph go, but NOT the copyright: it is now the only one on the page, so
     hiding it here would leave a phone with none. */
  .lg-pitch p, .lg-chips { display: none; }
  .lg-brandside .lg-copy { margin-top: 18px; }
  .lg-formside { padding: 28px 22px 44px; place-items: start center; }
}

/* ── recovery dialog (native <dialog>) ────────────────────────────────────────────────────────────
 * `::backdrop` and `showModal()` give focus trapping, Esc-to-close and an inert background with no
 * library. The panel is capped at 420px and allowed to scroll, so it stays usable on a 360px phone in
 * landscape where the viewport is barely taller than the form.
 */
.lg-dialog {
  border: 1px solid var(--line); border-radius: 14px; padding: 26px;
  width: calc(100% - 32px); max-width: 420px; max-height: 90vh; overflow: auto;
  background: var(--white); color: var(--ink); box-shadow: 0 24px 60px rgba(20, 32, 46, .28);
}
.lg-dialog::backdrop { background: rgba(15, 23, 42, .55); }
.lg-dialog h3 { margin: 0 0 6px; font-size: 20px; letter-spacing: -.02em; }
.lg-dialog-actions { display: flex; gap: 10px; justify-content: flex-end; align-items: center; margin-top: 6px; }
.lg-btn-quiet {
  min-height: 44px; padding: 0 16px; border: 1px solid var(--line); border-radius: 10px;
  background: var(--white); color: var(--ink-soft); font-size: 14.5px; font-weight: 600; cursor: pointer;
}
.lg-btn-quiet:hover { background: var(--sand); color: var(--ink); }
.lg-btn-quiet:focus-visible { outline: 2px solid #0f766e; outline-offset: 2px; }

/* A button that reads as a link, but is a real <button> because it performs an action rather than
   navigating — a bare <a href="#"> would be a lie to a screen reader and to the keyboard. */
.lg-link {
  border: 0; background: none; padding: 0; cursor: pointer;
  color: #0b6b68; font-size: 13.5px; font-weight: 600; text-decoration: underline;
  text-underline-offset: 2px; font-family: inherit;
}
.lg-link:hover { color: #0f766e; }
.lg-link:focus-visible { outline: 2px solid #0f766e; outline-offset: 3px; border-radius: 4px; }

/* Success variant of the login alert, which is otherwise styled as an error. */
.lg-alert-ok { background: #e6f4ec; border-color: #bfe3ce; color: #1f5c3d; }

@media (max-width: 900px) {
  .lg-dialog { padding: 20px; }
}

/* ── password policy checklist ────────────────────────────────────────────────────────────────────
 * Colour is never the only signal: the glyph itself changes ✗ → ✓, so the state survives a
 * colour-blind reader, a greyscale print and a high-contrast mode that discards the colour.
 */
.pw-rules { list-style: none; margin: -6px 0 18px; padding: 0; display: grid; gap: 5px; }
.pw-rules li {
  font-size: 13px; color: var(--ink-soft);
  display: grid; grid-template-columns: 18px 1fr; align-items: start; gap: 6px;
}
.pw-rules li.is-ok { color: var(--ok); }
.pw-mark { font-weight: 700; line-height: 1.45; color: var(--danger); }
.pw-rules li.is-ok .pw-mark { color: var(--ok); }
.pw-ok { color: var(--ok); }
.pw-bad { color: var(--danger); }

/* ── show/hide on password fields (injected by partials/password-reveal) ──────────────────────────
 * The wrapper is display:block so it inherits the .field layout the inputs already sit in; making it
 * inline-block would collapse the full-width inputs to their content size.
 */
.pw-wrap { position: relative; display: block; }
.pw-wrap input { padding-right: 44px; }
.pw-eye {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  width: 36px; height: 36px; border: 0; background: none; cursor: pointer;
  color: var(--ink-soft); display: grid; place-items: center; border-radius: 8px; padding: 0;
}
.pw-eye svg { width: 18px; height: 18px; }
.pw-eye:hover { background: var(--sand); color: var(--ink); }
.pw-eye:focus-visible { outline: 2px solid var(--teal-dark); outline-offset: 1px; }
/* Revealed state carries the brand colour as well as the aria-pressed state, so it is not colour-only:
   the label and aria-pressed both change too. */
.pw-eye.is-on { color: var(--teal-dark); }

/* ── account menu in the topbar ───────────────────────────────────────────────────────────────────
 * A <details>/<summary> disclosure, so it opens and closes with no JavaScript. The marker is removed
 * because the chevron carries that meaning and a native triangle beside it reads as a mistake.
 */
.acct { position: relative; flex: none; }
.acct > summary { list-style: none; cursor: pointer; }
.acct > summary::-webkit-details-marker { display: none; }
.acct-btn {
  display: flex; align-items: center; gap: 9px; padding: 5px 9px 5px 5px;
  border-radius: 999px; border: 1px solid transparent;
}
.acct-btn:hover { background: var(--sand); border-color: var(--line); }
.acct[open] > .acct-btn { background: var(--sand); border-color: var(--line); }
.acct > summary:focus-visible { outline: 2px solid var(--teal-dark); outline-offset: 2px; border-radius: 999px; }
.acct-name { line-height: 1.2; text-align: left; }
.acct-name strong { display: block; font-size: 13.5px; color: var(--ink); }
.acct-name small { display: block; font-size: 11px; color: var(--ink-soft); }
.acct-chev { width: 15px; height: 15px; color: var(--ink-soft); flex: none; }
.acct[open] .acct-chev { transform: rotate(180deg); }

/* Avatar: an image when there is one, otherwise the initial. Same box either way so the bar never
   shifts when a picture is added. */
.avatar {
  width: 32px; height: 32px; border-radius: 50%; flex: none; overflow: hidden;
  display: grid; place-items: center; background: linear-gradient(135deg, var(--teal), var(--teal-dark));
  color: #fff; font-weight: 700; font-size: 14px; line-height: 1;
}
.avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.avatar-lg { width: 96px; height: 96px; font-size: 34px; }
.avatar-initial { user-select: none; }

.acct-menu {
  position: absolute; right: 0; top: calc(100% + 8px); min-width: 232px; z-index: 40;
  background: var(--white); border: 1px solid var(--line); border-radius: 12px;
  box-shadow: 0 18px 44px rgba(20, 32, 46, .18); padding: 6px; display: grid; gap: 2px;
}
.acct-item {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 10px 12px; border: 0; background: none; border-radius: 8px;
  font: inherit; font-size: 14px; color: var(--ink); text-align: left; cursor: pointer; text-decoration: none;
}
.acct-item svg { width: 16px; height: 16px; color: var(--ink-soft); flex: none; }
.acct-item:hover { background: var(--sand); }
.acct-item:focus-visible { outline: 2px solid var(--teal-dark); outline-offset: -2px; }
.acct-item-danger { color: var(--danger); }
.acct-item-danger svg { color: var(--danger); }
.acct-menu form { margin: 0; }

/* Profile: picture beside its controls, stacking on a narrow screen. */
.avatar-edit { display: flex; align-items: center; gap: 18px; margin-bottom: 20px; flex-wrap: wrap; }

@media (max-width: 720px) {
  /* The name is the first thing to go: the avatar identifies the account and the bar is tight. */
  .acct-name { display: none; }
  .acct-menu { right: -4px; min-width: 210px; }
}

/* ── Dashboard ─────────────────────────────────────────────────────────────────────────────────────
   The KPI tile is a separate component from `.tile` (still used by the invoices screen) because it
   carries three things a plain tile does not: a comparison line, a warning state, and an empty state.
   Bolting those onto `.tile` would have changed the invoices totals strip as a side effect.           */

.sec-head { font-size: 13px; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-soft);
            margin: 26px 0 12px; font-weight: 700; display: flex; align-items: baseline; gap: 10px; }
.sec-head:first-of-type { margin-top: 4px; }
.sec-note { font-size: 11.5px; letter-spacing: .02em; text-transform: none; font-weight: 400;
            color: var(--ink-soft); opacity: .8; }

.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 14px; }
.kpi { display: flex; flex-direction: column; gap: 3px; background: var(--white); border: 1px solid var(--line);
       border-radius: var(--radius); padding: 15px 16px 14px; box-shadow: var(--shadow);
       text-decoration: none; color: inherit; position: relative;
       /* The left edge is the status channel: teal normally, amber for something needing attention,
          nothing at all when the tile is empty. Reading the colour is faster than reading the label. */
       border-left: 3px solid var(--teal); transition: box-shadow .15s ease, transform .15s ease; }
a.kpi:hover { box-shadow: 0 2px 4px rgba(20,32,46,.08), 0 12px 28px rgba(20,32,46,.09); transform: translateY(-1px); }
a.kpi:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.kpi.is-empty { border-left-color: var(--line); background: #fdfcfa; }
.kpi.kpi-warn { border-left-color: var(--gold); }

.kpi-label { font-size: 11.5px; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-soft); font-weight: 600; }
.kpi-value { font-size: 30px; font-weight: 700; letter-spacing: -.025em; line-height: 1.12; color: var(--ink);
             font-variant-numeric: tabular-nums; }
.kpi-money { font-size: 26px; }
/* Not a number: same slot, obviously not a figure, so nobody mistakes it for a total of zero. */
.kpi-none { font-size: 15px; font-weight: 500; color: var(--ink-soft); opacity: .7; font-style: italic; letter-spacing: 0; }
.kpi-hint, .kpi-delta { font-size: 11.5px; color: var(--ink-soft); margin-top: 3px; line-height: 1.45; }
.kpi-delta { font-weight: 600; display: flex; flex-wrap: wrap; gap: 5px; align-items: baseline; }
.kpi-delta .muted { font-weight: 400; opacity: .85; }
.kpi-up   { color: var(--ok); }
.kpi-down { color: var(--danger); }
.kpi-flat { color: var(--ink-soft); }

/* TWO per row, Hany's call 2026-08-13. It was repeat(auto-fit, minmax(320px, 1fr)), which on a desktop
   width fits three or four and shrinks every chart until the day labels crowd; a comparison is easier to
   read side by side in pairs. Fixed at 2 rather than auto-fit because auto-fit is the very thing that made
   the count depend on the window. minmax(0, 1fr) not 1fr: a bare 1fr refuses to shrink below its content and
   an SVG chart would then push the grid wider than the page. */
.chart-grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; margin-top: 22px; }
.chart { width: 100%; height: 190px; display: block; overflow: visible; }
.chart-grid { stroke: var(--line); stroke-width: 1; vector-effect: non-scaling-stroke; }
.chart-bar { transition: opacity .12s ease; }
.chart-bar:hover { opacity: .78; }
/* A day with no trading keeps its stub, drawn faintly: present on the axis, plainly not a value. */
.chart-bar.is-zero { fill: var(--line) !important; }
.chart-xlabel { font-size: 10px; fill: var(--ink-soft); font-family: inherit; }
/*
 * Every day is labelled at EVERY width. The fold-away rule that used to live here was removed the moment
 * it was measured: at 375px the chart is 289px wide and all fourteen labels sit side by side with no
 * overlap, so hiding half of them only took away what was asked for. If a future layout does squeeze
 * them, `.chart-xlabel.is-alt` still marks the in-between ones and a media query can hide them again —
 * but only once a measurement says they collide.
 */
.chart-empty { margin: 0; padding: 34px 8px; text-align: center; color: var(--ink-soft); font-size: 13.5px;
               font-style: italic; background: var(--sand); border-radius: 8px; border: 1px dashed var(--line); }
.chart-foot { display: flex; flex-wrap: wrap; gap: 16px; margin: 12px 0 0; padding-top: 10px;
              border-top: 1px solid var(--line); font-size: 12px; color: var(--ink-soft); }
.chart-foot strong { color: var(--ink); font-variant-numeric: tabular-nums; }

/* Horizontal breakdown. A grid rather than a flex row so the labels, values and shares line up in
   columns across every row — ragged numbers are unreadable when the point is comparison. */
.bd { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 9px; }
.bd-row { display: grid; grid-template-columns: 108px 1fr 74px 44px; gap: 10px; align-items: center; font-size: 13px; }
.bd-label { color: var(--ink); text-transform: capitalize; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bd-track { background: var(--sand); border: 1px solid var(--line); border-radius: 5px; height: 15px; overflow: hidden; }
.bd-fill { display: block; height: 100%; background: var(--teal); border-radius: 4px 0 0 4px; }
.bd-value { text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; color: var(--ink); }
.bd-share { text-align: right; font-size: 11.5px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
/* Status colours follow the life of a booking, and match the badges used on the booking screens. */
.bd-completed  .bd-fill { background: var(--ok); }
.bd-cancelled  .bd-fill { background: var(--danger); }
.bd-no_show    .bd-fill { background: #c9772f; }
.bd-checked_in .bd-fill { background: var(--teal-dark); }
.bd-booked     .bd-fill { background: var(--teal); }

.cfg-strip { display: flex; flex-wrap: wrap; gap: 10px; }
.cfg { display: flex; align-items: baseline; gap: 7px; background: var(--sand); border: 1px solid var(--line);
       border-radius: 999px; padding: 7px 15px; text-decoration: none; color: inherit; font-size: 12.5px; }
.cfg:hover { background: var(--white); border-color: var(--teal); }
.cfg:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.cfg-value { font-weight: 700; font-size: 15px; font-variant-numeric: tabular-nums; }
.cfg-label { color: var(--ink-soft); }

/* 360px is one of the four widths Phase 3 tested at. The breakdown label column has to shrink or the
   bar track collapses to nothing and the chart stops being a chart. */
/* Below a tablet, two columns of chart are narrower than the labels they carry: stack them. */
@media (max-width: 900px) {
  .chart-grid-2 { grid-template-columns: 1fr; }
}

@media (max-width: 560px) {
  .kpi-value { font-size: 26px; }
  .kpi-money { font-size: 22px; }
  /* 🔑 On a phone the 84px label column TRUNCATED the labels that carry the meaning: "City Ledger / AR"
     printed as "City Ledger / ..." and, worse, "Cash (Voided Bill)" as "Cash (Voide..." — losing the one
     word that explains why that row is 0.00. Wrapping instead of ellipsis costs a few pixels of row height
     and no information; shortening the bar to widen the label would have cost the comparison itself, which
     is the whole point of the chart. `align-items: start` keeps the wrapped label level with its bar. */
  .bd-row { grid-template-columns: 96px 1fr 58px 34px; gap: 7px; font-size: 12px; align-items: start; }
  .bd-label { white-space: normal; overflow: visible; text-overflow: clip; line-height: 1.25; }
  .bd-track { margin-top: 1px; }
  .chart { height: 165px; }
}

/* ---------- the nav below 900px: a menu panel, not a strip ----------
 * 🚨 WHY THE OLD ONE FAILED. Below 720px the nav was made a horizontally scrollable strip with
 * `width: 100%` — but `.nav` is a flex ITEM of a single-row bar, so `width` lost to `flex-basis` and the
 * strip only ever got the bar's leftover space: measured at 390px it was a **169px window onto 1360px of
 * links**, i.e. 18 items dragged sideways through a gap showing two. A phone needs the whole list.
 *
 * So the nav leaves the flex flow entirely and becomes a panel under the bar. Because it is absolutely
 * positioned it no longer competes with the brand for width, which is also what stopped the brand wrapping
 * to four lines.
 *
 * 🔑 THE BOUNDARY IS 1150px, AND IT WAS MEASURED, NOT PICKED. With the desktop nav, 18 links need 1280px to
 * sit in two rows: the bar measures 163px at 950px, 126px at 1200px and 89px from 1280px up. Even with the
 * pills tightened, 1025–1149px still needed THREE rows (116px of bar) — a sixth of a laptop screen spent on
 * navigation — so on Hany's call (2026-08-13) the panel now covers everything up to 1150px. From 1151 to
 * 1280 the links stay visible and the pills tighten instead (below); above that the full padding fits two
 * rows unaided.
 */
@media (max-width: 1150px) {
  .topbar { position: relative; flex-wrap: nowrap; }

  /* The bar is then: menu, brand, slack, account — the standard mobile app-bar order. `order` is used
     rather than moving the markup, because the checkbox must stay a PRECEDING SIBLING of .nav for
     `:checked ~ .nav` to reach it. */
  .navbtn { order: 0; }
  .brand { order: 1; min-width: 0; }
  .spacer { order: 2; }
  .acct, .whoami { order: 3; }

  .navbtn {
    display: grid; place-items: center; flex: none;
    width: 40px; height: 40px; border-radius: 10px;       /* 40px: a real tap target, not a 22px icon */
    color: var(--ink); cursor: pointer; user-select: none;
  }
  .navbtn:hover { background: var(--sand); }
  /* The focus ring belongs on the LABEL, since the input it belongs to is off-screen. */
  .nav-toggle:focus-visible + .navbtn { outline: 2px solid var(--teal); outline-offset: 2px; }

  .nav {
    display: none;
    position: absolute; top: 100%; left: 0; right: 0;
    flex-direction: column; flex-wrap: nowrap; gap: 2px;
    margin: 0; padding: 8px 12px 12px;
    background: var(--white); border-bottom: 1px solid var(--line);
    box-shadow: 0 12px 26px rgba(23, 43, 42, .12);
    /* Eighteen items will not fit a phone: the panel scrolls itself rather than the page. */
    max-height: calc(100vh - 100%); overflow-y: auto; -webkit-overflow-scrolling: touch;
  }
  .nav-toggle:checked ~ .nav { display: flex; }
  /* 44px-ish rows: the pill padding of a desktop nav is not a thumb target. */
  .nav a { padding: 11px 14px; border-radius: 10px; font-size: 15px; }
}

/* 1151–1280px: every link still shown, but the pills give up a little padding so 18 of them fit two rows
   (83px of bar) rather than three. Measured: 2 rows from 1280px up with the full padding anyway. */
@media (max-width: 1280px) and (min-width: 1151px) {
  .nav { gap: 1px; margin-left: 8px; }
  .nav a { padding: 6px 9px; font-size: 13.5px; }
}

/* The subline is 25 uppercase characters and wraps on a narrow phone; the name alone identifies the app,
   and the full lockup is still on the login page. */
@media (max-width: 560px) {
  .brand small { display: none; }
}

/* A payment against a voided bill, and a net-negative row (refunds outweighed takings). Both are real
   movements that must appear, and both must be impossible to mistake for revenue — hence the muted
   striped fill rather than the solid teal a genuine taking gets. */
.bd-void .bd-label, .bd-neg .bd-label { color: var(--ink-soft); font-style: italic; }
.bd-void .bd-fill {
  background: repeating-linear-gradient(45deg, var(--line), var(--line) 4px, #f2ece1 4px, #f2ece1 8px);
}
.bd-neg .bd-fill { background: var(--danger); opacity: .55; }
.bd-neg .bd-value { color: var(--danger); }

/* Discount panel: the two inputs read as one control ("20 percent"), so they share a row and collapse to
   stacked at narrow widths rather than squeezing to unusable. */
.disc-row { display: grid; grid-template-columns: minmax(120px, 1fr) minmax(110px, 1fr); gap: 12px; }
@media (max-width: 480px) { .disc-row { grid-template-columns: 1fr; } }


/* ── Calendar: therapist / room tabs ───────────────────────────────────────────────────────────────
   Pure CSS. The radios sit off-screen (not `hidden`, which would make them unfocusable) and the panels
   are shown by the sibling `:checked` combinator, so switching costs no request and no JavaScript. */
.cal-tabin { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
.cal-tablist { display: flex; gap: 6px; margin-bottom: 12px; border-bottom: 1px solid var(--line); }
.cal-tab {
  display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
  padding: 9px 16px; font-size: 14px; font-weight: 600; color: var(--ink-soft);
  border: 1px solid transparent; border-bottom: 0; border-radius: 8px 8px 0 0;
  margin-bottom: -1px; background: none;
}
.cal-tab:hover { color: var(--ink); background: var(--sand); }
.cal-tabn {
  font-size: 11.5px; font-weight: 700; padding: 1px 7px; border-radius: 999px;
  background: var(--sand); color: var(--ink-soft);
}
.cal-panel { display: none; }
/* Therapists is the default view: its radio carries `checked` in the markup. */
#cal-view-therapist:checked ~ .cal-tablist label[for="cal-view-therapist"],
#cal-view-room:checked      ~ .cal-tablist label[for="cal-view-room"] {
  color: var(--teal-dark); background: var(--white);
  border-color: var(--line); border-bottom: 1px solid var(--white);
}
#cal-view-therapist:checked ~ .cal-tablist label[for="cal-view-therapist"] .cal-tabn,
#cal-view-room:checked      ~ .cal-tablist label[for="cal-view-room"] .cal-tabn {
  background: var(--teal); color: #fff;
}
#cal-view-therapist:checked ~ .cal-panel[data-cal-panel="therapist"],
#cal-view-room:checked      ~ .cal-panel[data-cal-panel="room"] { display: block; }
/* Keyboard users must be able to see which tab they are on. */
.cal-tabin:focus-visible ~ .cal-tablist label[for="cal-view-therapist"],
.cal-tabin:focus-visible ~ .cal-tablist label[for="cal-view-room"] { outline: 2px solid var(--teal); outline-offset: -2px; }

/* ── The whole shift, no vertical scroll ───────────────────────────────────────────────────────────
   --slots is written on the grid by the view (48 for an 08:00-20:00 day at 15-minute slots). The row
   height is whatever divides the remaining viewport between them, so the last row of the day sits just
   above the fold instead of below it.

   clamp() bounds both ends: never below 11px (a 15-minute row stops being clickable), never above 26px
   (the old fixed height — on a tall screen a short day should not stretch into a sparse grid). On a very
   short window the 11px floor wins and the grid scrolls again, which is the honest fallback. */
.cal-grid { --rowh: clamp(11px, calc((100vh - 316px) / var(--slots, 48)), 26px); }

/* ---------- the calendar on a phone or a portrait tablet ----------
 * 🚨 CASCADE TRAP, and it had already silently killed the existing mobile rule. There was a
 * `@media (max-width: 720px) { .cal-grid { --rowh: clamp(11px, (100vh - 260px)/48, 26px) } }` block EARLIER
 * in this file. A media query adds NO specificity, so the unconditional rule above — being later — won at
 * every width, and the mobile override never applied. Mobile calendar rules therefore live HERE, after it.
 *
 * 🔑 The row is FIXED at 22px and the grid scrolls. "The full shift without scrolling" is a DESKTOP rule and
 * cannot hold on a phone: measured at 390x844 the clamp gave 11.5px a slot, which made every treatment block
 * shorter than its own text — "Hot Stone Massage 50 Min" was clipped mid-line — and left an empty slot an
 * 11px tap target. 48 x 22px = 1056px of grid the page scrolls through. Twelve hours legibly is worth a
 * scroll; twelve hours illegibly is worth nothing.
 */
@media (max-width: 900px) {
  .cal-grid { --rowh: 22px; }
  /* 20px pills in the all-day strip. A control you cannot hit is not a control. */
  .cal-off-btn { min-height: 32px; display: inline-flex; align-items: center; }
  /* A cell's link is what a finger aims at; 15px of inline text is not it. The table stays wider than the
     screen and scrolls inside its own container, which is the honest answer for nine columns of money. */
  table a, table a.mono { display: inline-block; padding: 8px 0; }
}
/* At the compressed end the event text has to shrink with the rows or it spills its block. */
.cal-ev { font-size: clamp(9.5px, calc(var(--rowh) * .52), 12px); line-height: 1.25; }

/* ---------- a chip shows what it has ROOM for, measured against its own height ----------
 * MEASURED at 1280x900: 8 of 11 chips clipped. A 50-minute booking is 3.33 slots, and with the row height
 * clamped to 12.5px on a window that tall the chip is 39.7px while its four lines (time / guest / service /
 * room) need 54px — so the room line, and half the service line, were cut off mid-word. It is not a phone
 * problem: rowh only reaches the 17.3px that four lines need at a viewport ~1,130px tall, so nearly every
 * laptop was clipping.
 *
 * 🔑 THE CONDITION IS THE CHIP'S OWN HEIGHT, WHICH IS WHY THIS IS A CONTAINER QUERY AND NOT A MEDIA QUERY.
 * The clipping depends on the booking's LENGTH and the window's height together — a 50-minute chip is fine at
 * 1,300px tall and clipped at 900px, while a 120-minute chip is fine at both. Only the element knows.
 *
 * Thresholds are the measured line box (11.875px at the 9.5px floor) against the container's CONTENT box,
 * which is the chip minus 6px padding and 2px border:
 *   >= 47px  four lines   time / guest / service / room
 *   >= 35px  three lines  room dropped — the least useful of the four at a glance
 *   >= 23px  two lines    time and guest share line one, service keeps line two
 *    < 23px  one line     time and guest only; the chip's POSITION already says when it is
 * Degrades to today's behaviour on a browser without container queries: it clips, exactly as it did before.
 */
.cal-ev { container-type: size; white-space: nowrap; }
/* 🚨 SELECTORS MUST OUT-SPECIFY THE BASE RULE. `@container` adds no specificity — the same trap as a media
   query — so a bare `.ev-room` (0,1,0) lost to `.cal-ev .ev-room { display: block }` (0,2,0) above and the
   first version of this block measured as doing absolutely nothing. Matched shape for shape. */
@container (max-height: 47px) { .cal-ev .ev-room { display: none; } }
@container (max-height: 35px) { .cal-ev .ev-guest { display: inline; } }
@container (max-height: 23px) {
  .cal-ev .ev-svc { display: none; }
  /* At one line there is no room for leading either: 1.25 of 9.5px does not fit in a 10.5px content box. */
  .cal-ev .ev-time, .cal-ev .ev-guest { line-height: 1; }
}
.cal-time { font-size: clamp(9px, calc(var(--rowh) * .46), 11.5px); }

/* ---------- the calendar's screen budget on a phone ----------
 * MEASURED at 390x900: the chrome above the grid was 448px — HALF the screen went on controls, so the day
 * opened showing 08:00 to about 12:15 and everything else was a scroll away. Every number below was taken
 * from that measurement rather than chosen.
 *
 * 🚨 `position: sticky` ON THE HEADER AND THE TIME GUTTER WAS DOING NOTHING HERE. Sticky pins to the nearest
 * SCROLLING ancestor, and `.cal-wrap` had its height cap removed (a desktop decision: show the whole shift
 * without scrolling). At 22px a row the grid is 1,128px tall, so on a phone the PAGE scrolled, not the box —
 * which meant the therapist names scrolled off the top and you were left reading an unlabelled grid, and the
 * sideways-scroll arrows, centred in a 1,128px box, sat ~560px below the fold where nobody would find them.
 * Capping the height on small screens is what turns all three back on: header pinned, times pinned, arrows
 * where a thumb is.
 */
@media (max-width: 720px) {
  /* Five summary tiles at `minmax(160px, 1fr)` fall to ONE column below 374px, which put 450px of totals
     between the filters and the first invoice — measured: at 360px the table started 1,050px down the page.
     Two per row, and the value type sized to match. */
  .tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-bottom: 16px; }
  .tile { padding: 11px 13px; }
  .tile-value { font-size: 20px; }
  .tile-label { font-size: 11px; }

  .cal-bar { gap: 8px; margin-bottom: 8px; }
  /* `.spacer { flex: 1 }` eats the rest of its line, which on a phone forced the legend and the Add-booking
     button onto two rows of their own. Dropped, they share one. */
  .cal-bar .spacer { display: none; }
  .cal-title { font-size: 17px; margin: 0; }
  .cal-meta { font-size: 12px; line-height: 1.35; margin: 6px 0 8px; }
  .legend { font-size: 11.5px; gap: 10px; }
  .cal-tab { padding: 10px 12px; font-size: 13.5px; }

}

/* The cap belongs at the SAME boundary as the fixed 22px row, not at 720px: a 768px tablet had the identical
 * defect — 1,126px of grid in an uncapped box, so the page scrolled, the pinned header and time gutter were
 * inert, and the scroll arrows sat 560px below the fold. Wherever the rows are fixed, the box is bounded.
 *
 * The height is chosen so the box BOTTOM lands near the fold (measured: the chrome above the grid is 408px on
 * a 390px phone, 307px on a 768px tablet). If the box ran past the fold the page would scroll, and scrolling
 * the page lifts the box — pinned header and all — off the top of the screen, which is the very thing the cap
 * is here to prevent. min-height keeps a landscape phone usable rather than letting the calc go negative.
 */
@media (max-width: 900px) {
  .cal-wrap { max-height: calc(100vh - 400px); min-height: 320px; }

  /*
   * The ‹ arrow overlapped the sticky time gutter by 29px of its 62px, covering half the hour labels it
   * is meant to help you read — so it was pushed to 68px, which cleared the gutter by standing on the
   * first therapist's lane instead. With the scroller now carrying a gutter of its own, neither is
   * necessary: the arrow sits beside the grid at both ends and covers nothing. The smaller button needs
   * a smaller gutter, which gives a phone back 20px of grid per side compared with the 68px placement.
   */
  .cal-arrow { width: 38px; height: 38px; font-size: 16px; }
  .cal-scroller { padding-inline: 24px; }
}

/* The Print action on the invoice head. */
.inv-print { margin: 0 0 14px; }

/* ── Calendar: drag to relocate ─────────────────────────────────────────────────────────────────────
   Feedback while dragging. The drop target is a whole slot rather than a pixel, so it is highlighted as a
   band across the column the pointer is over — at ~16px a row the cursor alone is not enough to tell a
   receptionist where a treatment is about to land. */
.cal-ev[draggable="true"] { cursor: grab; }
.cal-ev.is-dragging { opacity: .45; cursor: grabbing; box-shadow: none; }
.cal-slot.is-drop {
  background: rgba(15, 138, 134, .18);
  box-shadow: inset 0 2px 0 var(--teal), inset 0 -2px 0 var(--teal);
}

/* ── Booking lines: one row, one line high ──────────────────────────────────────────────────────────
   The per-treatment actions used to wrap one button per line in a narrow column, making every row ~167px
   tall and pushing the table wider than its card. A package booking now produces a row per session, so a
   five-session package put the Billing section below the fold. One primary action stays inline and the rest
   sit behind a "More" disclosure, which keeps a row to a single line. */
.line-actions { flex-wrap: nowrap; align-items: center; }
/* 🔑 THE MENU FLOATS, so opening it does not change the row's height. `.pop` is otherwise an in-flow box
   (margin-top: 10px), which would push the row taller the moment a clerk opened it — reintroducing exactly
   the problem this change removes. Anchored to the right so a menu on the last column cannot widen the
   table either. */
.row-more { position: relative; }
.row-more > .pop {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 20;
  margin-top: 0; background: var(--white); box-shadow: var(--shadow); text-align: left;
  /* Shrink-to-fit means a sentence of explanation decides the width: the cash reconcile panel came out
     717px and hung off the right of the screen at 1280. A menu is a small thing by definition. */
  max-width: 320px;
  /* 🚨 AND ONCE CAPPED, THE SENTENCE HAS TO WRAP. Every table cell sets `white-space: nowrap`, which the
     panel inherits, so "A variance needs an explanation before it can be signed off" ran out of the side
     of a 320px box and was cut off mid-word — the same disappearing text, one axis over. Seen in a
     screenshot; no measurement of the panel's own box would have caught it. */
  white-space: normal;
}
/* A booking can hold a ten-session package; tighter rows keep the whole treatment list on one screen. */
.booking-lines th, .booking-lines td { padding: 7px 12px; }
.booking-lines td .hint { margin-top: 0; }

/* 🔑 THE TREATMENT NAME IS THE ONLY CELL ALLOWED TO WRAP. Every cell inherits `white-space: nowrap`, so with
   seven columns and a name like "Face & Scalp Massage 50 Min (1 of 4)" beside a therapist like "Tamer Salem
   Mohamed" the table was wider than its card and the actions column was clipped off the right edge — the
   "More" button rendered as "M". Letting this one column reflow gives the table somewhere to give. */
.booking-lines td:first-child, .booking-lines th:first-child { white-space: normal; min-width: 190px; }
/* The actions column must never be the one that shrinks: it is the one being cut off. */
.booking-lines td:last-child, .booking-lines th:last-child { white-space: nowrap; width: 1%; }

/* ── Calendar: the all-day off-duty strip ───────────────────────────────────────────────────────────
   One strip per therapist column, between the name and the lane, so a therapist can be marked off for the
   day from the screen you are already on. The gutter carries an identical spacer, or the time axis slips out
   of step with the lanes and every appointment reads at the wrong time. */
.cal-offbar {
  display: flex; align-items: center; gap: 6px; height: 26px; padding: 0 6px;
  border-bottom: 1px solid var(--line); background: #fcfbf8; font-size: 11px;
  position: sticky; top: var(--headh, 38px); z-index: 3;
}
.cal-gutter .cal-offbar { z-index: 5; background: var(--white); }
.cal-offbar-spacer { justify-content: flex-end; }
.cal-off-tag {
  text-transform: capitalize; font-weight: 700; color: var(--danger);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cal-off { position: relative; }
/*
 * 🚨 THIS IS A BUTTON AND IT WAS DRESSED AS A BADGE. A solid pill reading "day off" sat under every
 * therapist's name all day — the same shape this app uses for statuses everywhere else — so a lane with
 * four treatments in it appeared to say the therapist was off. It is the control that MARKS them off.
 * Dashed, so at a glance it is a thing you press rather than a thing you are being told; the label now
 * starts with a verb, and a real absence still shows as `.cal-off-tag` in solid red.
 */
.cal-off-btn {
  cursor: pointer; color: var(--ink-soft); border: 1px dashed var(--line); border-radius: 999px;
  padding: 1px 8px; background: var(--white); white-space: nowrap; font-size: 10.5px;
}
.cal-off-btn:hover { color: var(--ink); border-color: var(--teal); }
/* Floats over the grid: an in-flow panel here would push the whole lane down and shift the time axis. */
.cal-off > .pop {
  position: absolute; left: 0; top: calc(100% + 5px); z-index: 30; margin-top: 0;
  background: var(--white); box-shadow: var(--shadow); min-width: 240px; font-size: 13px;
}
/*
 * 🚨 AND WHEN THE SCRIPT PINS IT, THIS MUST WIN — which means being written AFTER the rule above.
 * Placed with the other pinned-popover CSS (400 lines earlier) it lost the cascade to `position:
 * absolute` here, so the panel kept its absolute origin while the script fed it VIEWPORT coordinates:
 * every day-off panel jumped a few hundred pixels down and to the right. It measured as "off-screen
 * everywhere" — worse than the single off-screen lane it was meant to fix.
 */
.cal-off > .pop.is-pinned {
  position: fixed; right: auto; bottom: auto; margin: 0; transition: none;
  max-height: calc(100vh - 140px); overflow-y: auto;
  /* Pinned to the viewport it has no lane to size against, so it shrink-to-fits the whole window: the
     day-off form came out 639px wide for two fields. Same ceiling as the row menus. */
  max-width: 320px;
}

/* ── Reports ───────────────────────────────────────────────────────────────────────────────────────
   Hany, 2026-08-15: "the old system https://spa.sunrise-resorts.com/reports design was better and
   more clear than now". It was, and the cause was that the rebuild had ONE generic report view
   building its table head from the raw database column names, with no landing page, no totals and no
   per-report treatment. This block is the presentation half of the fix; `ReportCatalog` is the other.

   Everything here reuses the app's existing components where one exists — `.card`, `.field`, `.kpi`,
   `.pill`, `.btn` — and only adds what genuinely has no equivalent: the report chooser cards, the
   filter grid, the totals row, and the Summary screen's headline and ledger.                        */

/* ---------- the filter bar ----------
 * A `.card` rather than the old `.filterbar`, because these controls now number up to seven and a
 * flex row wrapped them into a ragged two-line block with no alignment between the lines. The grid
 * keeps the labels on one baseline and the controls on another, which is what makes a row of filters
 * scannable rather than merely present.
 *
 * `align-items: end` matters: the fields have labels of differing heights and without it the inputs
 * step up and down across the row. */
.rep-filter { padding: 16px 18px; margin-bottom: 18px; }
/* 🔑 The column max is 190px, NOT `1fr`. With `1fr` the tracks divide the whole card, so the landing
   page — which has only three filters — rendered a pair of 300px-wide date boxes with an inch of air
   inside each, and the same rule gave the report screen seven tracks that squeezed. A capped track
   plus `justify-content: start` makes a control the size its content needs on every screen, and the
   row simply wraps when there are too many. */
.rep-filter-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 190px));
  gap: 12px 14px; align-items: end; justify-content: start;
}
.rep-filter .field { margin-bottom: 0; }
/*
 * ⚠️ ALIGNED AT BOTH ENDS. `align-items: end` lined the CONTROLS up and let the labels fall wherever the
 * control's height put them — and the controls are not all the same height (a searchable outlet select,
 * a native status select, two date inputs), so the row of labels came out ragged: SEARCH, OUTLET and
 * HOTEL on one line, STATUS three pixels below, FROM two above. Each field is now a column with its
 * label at the top and its control pushed to the bottom, and every control is the same height, so both
 * rows line up whatever mix of filters a report declares.
 */
.rep-filter-grid { align-items: stretch; }
/* ⚠️ `.field-actions` IS a `.field`. Making every field a column stacked Run above Reset — the buttons
   are a row and stay one. Excluded rather than special-cased afterwards, so the two rules cannot fight. */
.rep-filter .field:not(.field-actions) { display: flex; flex-direction: column; }
.rep-filter .field > label { margin-bottom: 5px; }
.rep-filter .field > input,
.rep-filter .field > select,
.rep-filter .field > .ss { margin-top: auto; }
.rep-filter .field-actions { flex-direction: row; align-items: center; align-self: end; }
.rep-filter .field input,
.rep-filter .field select,
.rep-filter .field .ss > select { min-height: 38px; }
.rep-filter .field label {
  display: block; font-size: 11.5px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-soft); font-weight: 600; margin-bottom: 5px;
}
/* The search box earns two columns: it holds the longest values anyone types here. */
.rep-filter .field-wide { grid-column: span 2; }
.rep-filter .field-actions { display: flex; gap: 8px; align-items: center; }
@media (max-width: 640px) {
  .rep-filter .field-wide { grid-column: span 1; }
  .rep-filter .field-actions { grid-column: 1 / -1; }
  .rep-filter .field-actions .btn { flex: 1; text-align: center; }
}

/* ---------- the report chooser ----------
 * The landing page the rebuild never had. A card states what the report ANSWERS, which is how people
 * choose one; a tab strip can only state what it is called. */
.rep-cards {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(310px, 1fr));
  gap: 14px; margin-bottom: 8px;
}
.rep-card {
  display: flex; gap: 14px; align-items: flex-start; text-decoration: none; color: inherit;
  background: var(--white); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 16px 17px;
  transition: box-shadow .15s ease, transform .15s ease, border-color .15s ease;
}
.rep-card:hover {
  transform: translateY(-1px); border-color: var(--teal);
  box-shadow: 0 2px 4px rgba(20,32,46,.08), 0 12px 28px rgba(20,32,46,.09);
}
.rep-card:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.rep-card-icon {
  flex: none; width: 40px; height: 40px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
}
.rep-card-icon svg { width: 21px; height: 21px; }
.rep-card-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.rep-card-title { font-weight: 700; font-size: 15px; color: var(--ink); }
.rep-card-blurb { font-size: 12.5px; color: var(--ink-soft); line-height: 1.5; }

/* The tone is carried on the CARD and inherited by the icon through `currentColor`, so a report's
   colour is one class in the catalog rather than a pair of values repeated in the markup. */
.tone-emerald .rep-card-icon { background: #e6f4ec; color: #2f7a4f; }
.tone-teal    .rep-card-icon { background: #e2f2f1; color: var(--teal-dark); }
.tone-amber   .rep-card-icon { background: #fdf3e0; color: #96650f; }
.tone-gold    .rep-card-icon { background: #fbf3e0; color: #7a5f1a; }
.tone-sky     .rep-card-icon { background: #e6f0fa; color: #2b5f92; }
.tone-rose    .rep-card-icon { background: #fdeaea; color: #a8403c; }
.tone-violet  .rep-card-icon { background: #f0ebfa; color: #5b4494; }
.tone-slate   .rep-card-icon { background: #f0eee9; color: var(--ink-soft); }

/* ---------- the way back ----------
 * The in-report tab strip is GONE (Hany, 2026-08-15). With the landing page grouping the reports into
 * cards, a row repeating all eleven names on every report screen was a second chooser that duplicated
 * the first, and it pushed the filters and the first row of data down every page. The breadcrumb does
 * the one job it was really doing.
 *
 * 🔑 So the breadcrumb has to look like a CONTROL. `.crumb a` inherits `--ink-soft`, which made the
 * link indistinguishable from the static text beside it — tolerable when it was a convenience, wrong
 * when it is the only route back. It gets the link colour, a hit area, and a hover state. */
.rep-crumb { font-size: 13.5px; margin-bottom: 6px; display: flex; align-items: center; gap: 8px; }
.rep-crumb a {
  color: var(--teal-dark); font-weight: 600; text-decoration: none;
  padding: 3px 8px 3px 6px; margin-left: -6px; border-radius: 7px;
}
.rep-crumb a:hover { background: var(--sand); color: var(--teal); text-decoration: underline; }
.rep-crumb a:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }
/* The chevron is a hair large at the same size as the text, and it is decorative — aria-hidden in the
   markup, dimmed here, so it never reads as part of the link's name. */
.rep-crumb a span { opacity: .7; font-size: 15px; line-height: 1; }
.rep-crumb .crumb-sep { color: var(--line); }
.rep-crumb .crumb-here { color: var(--ink-soft); }

/* ---------- totals ----------
 * 🚨 ONE ROW PER CURRENCY — see ReportPresenter::totals(). The gold ground is the same one the
 * spreadsheet export uses for its totals row, so a printed sheet, a screen and a workbook all mark
 * the same rows the same way. */
.rep-table tfoot .rep-total td {
  background: #fbf6e9; font-weight: 700; border-top: 2px solid var(--line); border-bottom: none;
  position: sticky; bottom: 0;
}
.rep-table tfoot .rep-total:first-child td { border-top: 2px solid #d9cfae; }

/* The breakdown toggle sits hard right in the card head, away from the row count. */
.rep-extras { margin-left: auto; }

/* ---------- converted totals ----------
 * The head STACKS. `.card-head` is a flex row and its note ("converted at the rates set in Settings")
 * is long relative to a 420px card, so side by side the title wrapped to two lines and the note to
 * two more. A caption belongs under its heading at this width, not beside it. */
.rep-ccy { margin-top: 18px; max-width: 460px; }
.rep-ccy .card-head { flex-direction: column; align-items: flex-start; gap: 2px; }

/* ---------- the Summary headline ----------
 * The one figure the screen exists to show. It is deliberately NOT a `.kpi`: a tile in a grid of
 * tiles reads as one of several, and this is the number the others explain. */
.rep-headline {
  display: flex; align-items: center; gap: 18px; margin-top: 16px;
  background: #f0f8f4; border: 2px solid #cfe7db; border-radius: var(--radius); padding: 20px 22px;
}
.rep-headline-mark {
  flex: none; width: 52px; height: 52px; border-radius: 999px; background: var(--white);
  color: var(--ok); display: inline-flex; align-items: center; justify-content: center;
}
.rep-headline-mark svg { width: 26px; height: 26px; }
.rep-headline-label {
  font-size: 11.5px; text-transform: uppercase; letter-spacing: .06em; font-weight: 700; color: #2f6b4f;
}
.rep-headline-value {
  font-size: 38px; font-weight: 700; letter-spacing: -.03em; line-height: 1.1;
  color: #1f5c3f; font-variant-numeric: tabular-nums;
}
/* The definition, beside the number. "Actual paid" with no statement of whether it is net of refunds
   is exactly the figure that gets misread into a board pack. */
.rep-headline-note { font-size: 12px; color: #46705c; margin-top: 3px; }

@media (max-width: 520px) {
  .rep-headline { gap: 14px; padding: 16px; }
  .rep-headline-value { font-size: 30px; }
}

/* ---------- the reconciliation ledger ----------
 * Legacy's "How it adds up", kept because it is the single clearest thing that screen did: it shows
 * the arithmetic instead of asking you to trust it. */
.rep-ledger {
  margin-top: 14px; background: #fbfaf7; border: 1px solid var(--line);
  border-radius: var(--radius); padding: 13px 16px; font-size: 13.5px;
}
.rep-ledger-head { font-weight: 700; color: var(--ink-soft); margin-bottom: 4px; font-size: 12.5px; }
.rep-ledger-sum { font-variant-numeric: tabular-nums; line-height: 1.7; }
.rep-ledger-ok { color: var(--ok); font-weight: 700; margin-left: 8px; }
/* 🚨 An unbalanced ledger is LOUD. The failure mode this guards against is a tick printed over a
   discrepancy — the report would then be evidence that the books balance when they do not. */
.rep-ledger.is-off { background: #fdf6ee; border-color: #ebd9b4; }
.rep-ledger-off { color: #8A5A10; font-weight: 700; margin-left: 8px; }
.rep-ledger-note { margin-top: 6px; font-size: 12px; color: var(--ink-soft); line-height: 1.5; }

/* A report whose numbers need their basis stated says so under the table, not in a tooltip nobody
   opens. Utilisation is the case that forced it: a percentage is meaningless without its denominator. */
.rep-footnote {
  margin: 12px 2px 0; font-size: 12.5px; color: var(--ink-soft); line-height: 1.55; max-width: 92ch;
}

/* ---------- searchable-select panel: escape the card ----------
 * 🚨 `.ss-panel` was `position: absolute`, so ANY ancestor with `overflow: hidden` clipped it — and
 * `.card` has exactly that. Hany, 2026-08-15: "the selects not shown when clicked". The Outlet and
 * Hotel pickers carry 19 options each, so both are enhanced, and both sit inside the filter `.card`:
 * the panel opened every time and was cut off at the card's edge. Latent on every other screen that
 * puts an enhanced select in a card too — bookings, invoices, cash, stock, purchasing, staff time.
 *
 * Fixed positioning is relative to the viewport, so no ancestor can clip it. The coordinates are set
 * by searchable-select.js from the BUTTON's rect on open, and again on scroll and resize.
 *
 * ⚠️ This overrides the earlier `.ss-panel` block rather than editing it, because that block is the
 * component's own and this file is appended after it. `top`/`left` must be left to the JS — declaring
 * them here would fight it every time the panel is placed.
 */
.ss-panel {
  position: fixed;
  right: auto;
  /* min-width is set from the field's measured width; the max keeps a long outlet name on screen. */
  width: max-content; max-width: min(460px, calc(100vw - 24px));
}

/* ---------- a labelled filter bar (shared) ----------
 * `.filterbar` is a bare flex row of unlabelled controls, which is fine for a single search box and
 * stops being fine the moment a second control joins it: an unlabelled input beside a dropdown gives
 * no way to tell what either one filters. Added for the Staff screen's hotel filter (Hany, 2026-08-15).
 *
 * ⚠️ This is the SECOND labelled-filter pattern in the stylesheet — `.rep-filter-grid` is the reports
 * one, which uses a grid because it carries up to seven controls. They deliberately stay separate:
 * a two-control bar wants to hug its content, and forcing it into the reports grid would stretch two
 * fields across the page. If a third screen needs this, unify them then rather than adding a third.
 */
.filterbar-labelled { align-items: flex-end; gap: 14px; }
.filterbar-labelled .fb-field { display: flex; flex-direction: column; gap: 5px; }
.filterbar-labelled .fb-field label {
  font-size: 11.5px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-soft); font-weight: 600;
}
.filterbar-labelled .fb-actions { display: flex; gap: 8px; align-items: center; }
/* The enhanced (searchable) control replaces the select with a button, and a button in a flex row
   collapses to its label width — so the field keeps a floor wide enough for an outlet-length name.
   The search input takes the same floor: two controls of visibly different widths in a two-control bar
   read as an accident rather than a choice. */
.filterbar-labelled .fb-field select,
.filterbar-labelled .fb-field input[type=search],
.filterbar-labelled .fb-field .ss { min-width: 210px; }
@media (max-width: 640px) {
  .filterbar-labelled { align-items: stretch; }
  .filterbar-labelled .fb-field, .filterbar-labelled .fb-actions { width: 100%; }
  .filterbar-labelled .fb-field select, .filterbar-labelled .fb-field .ss,
  .filterbar-labelled .fb-field input { width: 100%; min-width: 0; }
}

/* ---------- a row's actions must stay IN the row ----------
 * 🚨 `.row-actions` is `display: flex`, and it was being put on a `<td>`. A table cell told to be a
 * flex container STOPS BEING A CELL — it leaves the row's cell model, so it no longer shares the row's
 * height or alignment and its buttons float at a different height from the data beside them. That is
 * the users list "rows not aligned with their action buttons" (Hany, 2026-08-15); the invoices list
 * carries the same markup and had the same fault.
 *
 * The class is right on a `<div>` (page heads, form feet) — so this restores cell behaviour only where
 * it lands on a cell, rather than changing `.row-actions` and disturbing everywhere it is correct.
 */
/*
 * 🚨 THE SAME FAULT, A SECOND CLASS. `.hint { display: block }` (no media query) is on
 * `<td class="muted hint u-outlets">` in the users list — so the Outlets cell was a BLOCK at every
 * width while its neighbours were cells. It sat 3-4px lower, 5-10px shorter than the row, and because
 * every `td` carries `border-bottom: 1px`, that border drew at the cell's own y instead of the row's:
 * "the outlets has its own border" (Hany, 2026-08-15, after the first alignment fix).
 *
 * Measured on the live page: cell 3 top=419/h=77 against the row's 415/81; the other five cells were
 * exact. Found by walking every stylesheet for rules matching that cell and declaring `display` —
 * guessing which rule wins is how you end up fixing the wrong one.
 *
 * `.hint` is on a `<td>` in FOUR places (users, audit, postings x2), so all four had it.
 */
td.row-actions, td.hint, th.hint { display: table-cell; vertical-align: middle; }

/* Every column on the same baseline. Without this the cell contents align on the FIRST TEXT BASELINE,
   so a two-line name column and a single row of buttons sit at visibly different heights. */
.cards-sm th, .cards-sm td { vertical-align: middle; }

/* ⚠️ Re-asserted AFTER the rule above, because below the breakpoint each row becomes a card and the
   cells genuinely are blocks. Without this the fix would break the phone layout it does not concern. */
/* ⚠️ Both re-asserted below the card breakpoints, where the cells genuinely are blocks. `.hint` needs
   BOTH: `.cards-sm` cards below 1280px, `.set-table` below 720px. */
@media (max-width: 1280px) {
  .cards-sm td.row-actions, .cards-sm td.hint { display: block; }
}
@media (max-width: 720px) {
  .set-table td.hint { display: block; }
}

/* ---------- the permissions grid ----------
 * One table per domain, so the columns only line up if they are told to. `fixed` plus explicit widths
 * gives every card the same geometry; the name column takes whatever is left. */
.perm-table { table-layout: fixed; }
.perm-table th, .perm-table td { vertical-align: middle; }
.perm-table th:nth-child(2), .perm-table td:nth-child(2) { width: 150px; }
.perm-table th:nth-child(n+3), .perm-table td:nth-child(n+3) { width: 118px; text-align: center; }
/* The permission name is the one column that may wrap — a fixed layout would otherwise clip a long
   name rather than growing the row. */
.perm-table td:first-child { white-space: normal; }

/* 🔑 `.check` carries `margin-bottom: 20px` for stacked form checkboxes. Inside a table cell that is
   dead space under every radio, which made each row 20px taller than its content and pushed the radio
   off the centre line it shares with the name beside it. */
.perm-table .check {
  margin-bottom: 0; justify-content: center; flex-wrap: wrap; gap: 4px;
}
/* "you do not hold this" gets its own centred line instead of shunting the radio off-centre. */
.perm-table .check .hint { flex-basis: 100%; text-align: center; margin: 0; }

/* ---- the permission grids on a narrow screen ------------------------------------
 * "What X may do" and "apply to many" are the same job — one permission per row, one radio per
 * decision — so they share `.perm-table` geometry now instead of one being fixed and one auto-sized.
 *
 * 🚨 THE REVOKE COLUMN WAS OFF THE CARD AT 480 (74px on the permissions page, 204px on the bulk grid).
 * On a page whose entire purpose is granting and revoking, half the decision was off-screen.
 *
 * 🔑 And sideways scroll alone would NOT have fixed it: the permission NAME scrolls away with the rest,
 * leaving three unlabelled radio buttons with no way to tell which permission you are changing — the
 * one screen in the app where picking the wrong row hands somebody money or master access. So the name
 * column stays put and the decisions scroll under it, the same treatment `.line-table` gets at 700px.
 */
@media (max-width: 700px) {
  /* 🚨 A FLOOR, OR THE NAME COLUMN PAYS FOR THE FIT. Without it the table dutifully squeezed itself into
     375px by crushing the permission name to 39px — six lines per name and 192px-tall rows — and the
     overflow check passed, because nothing was cut. Below this width the table scrolls instead, which is
     precisely when the sticky name column earns its keep.
     430px because that is the MEASURED card interior at a 480px viewport — the grid fits exactly there and
     starts scrolling only where it genuinely cannot fit (364px of room at 414, 325px at 375). */
  .perm-table { min-width: 430px; }
  .perm-table th:nth-child(2), .perm-table td:nth-child(2) { width: 100px; }
  .perm-table th:nth-child(n+3), .perm-table td:nth-child(n+3) { width: 62px; }
  .perm-table th, .perm-table td { padding-left: 8px; padding-right: 8px; }

  /* 🚨 `position: sticky` is inert on a cell of a `border-collapse: collapse` table in Chrome — the rule
     applies, the cell does not stick, and it looks right in the stylesheet while doing nothing. */
  .perm-table { border-collapse: separate; border-spacing: 0; }
  .perm-table th, .perm-table td { border-bottom: 1px solid var(--line); }
  .perm-table th:first-child, .perm-table td:first-child {
    position: sticky; left: 0; background: var(--white);
    box-shadow: 7px 0 7px -7px rgba(31, 27, 22, .25);
  }
  .perm-table thead th:first-child { background: var(--sand); }
  /* A domain heading spans the whole row, so pinning it would park it over the decisions it introduces. */
  .perm-table td.group-row { position: static; box-shadow: none; }
}

/* --- Outlets: the "hotels served" picker (2026-08-28) -----------------------------------------
   Scoped to the dialog on /outlets. Uses the sheet's own tokens (--line, --ink-soft, --sand) so it
   follows the theme rather than pinning its own greys. */
.hotels-pick { max-height: 46vh; overflow-y: auto; border: 1px solid var(--line); border-radius: 10px; }
.hotels-pick-row { display: flex; align-items: center; gap: 10px; padding: 9px 12px; cursor: pointer; margin: 0; }
.hotels-pick-row + .hotels-pick-row { border-top: 1px solid var(--line); }
.hotels-pick-row:hover { background: var(--sand); }
.hotels-pick-name { flex: 1 1 auto; min-width: 0; }
.hotels-pick-first { display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto;
  font-size: 12px; color: var(--ink-soft); white-space: nowrap; }
.hotels-pick-first input[disabled] { opacity: .35; }
.hotels-pick-first input[disabled] + span { opacity: .35; }

/* ── Header menu, grouped (2026-10-11) ─────────────────────────────────────────────────────────────
 * Hany: "check the header menu design it need some work". 19 flat links wrapped to two rows even at
 * 1920px. The daily screens stay as pills; Stock / Catalogue / Admin are <details> dropdowns styled like
 * the account menu, so the bar is ONE row (60px) again on every desktop width. ≤1150px keeps the existing
 * hamburger, now driving .nav-mobile (all entries, with section headings).
 */
.nav { flex-wrap: nowrap; align-items: center; }
.nav a, .navgrp > summary { white-space: nowrap; }
.navgrp { position: relative; flex: none; }
.navgrp > summary {
  list-style: none; cursor: pointer; user-select: none;
  display: flex; align-items: center; gap: 5px;
  padding: 7px 11px 7px 13px; border-radius: 999px;
  color: var(--ink-soft); font-weight: 500; font-size: 14px;
}
.navgrp > summary::-webkit-details-marker { display: none; }
.navgrp > summary:hover, .navgrp[open] > summary { background: var(--sand); color: var(--ink); }
.navgrp > summary.is-current { background: var(--ink); color: #fff; }
.navgrp > summary:focus-visible { outline: 2px solid var(--teal-dark); outline-offset: 2px; }
.navgrp-chev { width: 14px; height: 14px; flex: none; opacity: .7; transition: transform .15s; }
.navgrp[open] .navgrp-chev { transform: rotate(180deg); }
.navgrp-menu {
  position: absolute; left: 0; top: calc(100% + 8px); min-width: 210px; z-index: 40;
  background: var(--white); border: 1px solid var(--line); border-radius: 12px;
  box-shadow: 0 18px 44px rgba(20, 32, 46, .18); padding: 6px; display: grid; gap: 1px;
}
.nav .navgrp-menu a {
  display: block; padding: 9px 12px; border-radius: 8px; font-size: 14px; color: var(--ink);
}
.nav .navgrp-menu a:hover { background: var(--sand); }
.nav .navgrp-menu a[aria-current="page"] { background: var(--sand); color: var(--teal-dark); font-weight: 600; box-shadow: inset 3px 0 0 var(--teal); }
.navgrp-title {
  padding: 10px 12px 4px; font-size: 10.5px; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--ink-soft);
}
.navgrp-title:first-child { padding-top: 6px; }
.navgrp-menu .navgrp-title:not(:first-child) { border-top: 1px solid var(--line); margin-top: 4px; }

.nav-mobile { display: none; }

@media (max-width: 1280px) and (min-width: 1151px) {
  .navgrp > summary { padding: 6px 9px 6px 10px; font-size: 13.5px; }
}

@media (max-width: 1150px) {
  /* the desktop nav (with its dropdowns) never shows here; the panel below replaces it */
  .nav, .nav-toggle:checked ~ .nav { display: none !important; }
  .nav-mobile {
    display: none;
    position: absolute; top: 100%; left: 0; right: 0;
    flex-direction: column; gap: 2px;
    margin: 0; padding: 8px 12px 12px;
    background: var(--white); border-bottom: 1px solid var(--line);
    box-shadow: 0 12px 26px rgba(23, 43, 42, .12);
    max-height: calc(100vh - 100%); overflow-y: auto; -webkit-overflow-scrolling: touch;
  }
  .nav-toggle:checked ~ .nav-mobile { display: flex; }
  .nav-mobile a {
    padding: 11px 14px; border-radius: 10px; font-size: 15px; font-weight: 500;
    color: var(--ink-soft); text-decoration: none;
  }
  .nav-mobile a:hover { background: var(--sand); color: var(--ink); }
  .nav-mobile a[aria-current="page"] { background: var(--ink); color: #fff; }
  .nav-mobile-title {
    padding: 14px 14px 4px; font-size: 11px; font-weight: 700; letter-spacing: .08em;
    text-transform: uppercase; color: var(--ink-soft); border-top: 1px solid var(--line); margin-top: 6px;
  }
}

/* ── List screens: Hotels / Outlets (2026-10-11) ────────────────────────────────────────────────────
 * Hany: "check also the hotels and outlets page design they need some work". Hotels had its three row
 * buttons stacked (117px rows, a 4,470px page) beside two all-empty columns; Outlets was 2,826px wide in a
 * 1,310px card, so code, money, counts, status and its buttons were off-screen. Now: secondary facts sit
 * under the main one (.cell-sub), lists are chips, buttons share one row, and a search box filters rows.
 */
.list-table td { vertical-align: middle; }
.cell-main { display: block; color: var(--ink); }
.cell-sub { display: block; font-size: 12px; color: var(--ink-soft); margin-top: 2px; }
.cell-sub .mono { font-size: 11.5px; }
.chips { display: flex; flex-wrap: wrap; gap: 4px; white-space: normal; max-width: 380px; }
.chip { display: inline-block; padding: 2px 9px; border-radius: 999px; font-size: 12px; line-height: 1.6;
        background: #f3f0eb; color: var(--ink-soft); white-space: nowrap; }
.chip-first { background: #e6f4ef; color: var(--teal-dark); font-weight: 600; }
.chip-more { background: transparent; border: 1px dashed var(--line); }
.row-btns { display: inline-flex; gap: 6px; align-items: center; justify-content: flex-end; }
.card-head .list-search {
  margin-left: auto; height: 34px; box-sizing: border-box; width: 260px; max-width: 40vw; padding: 7px 14px;
  border: 1px solid var(--line); border-radius: 999px; font: inherit; font-size: 13px; background: var(--white);
}
.card-head .list-search:focus { outline: 2px solid var(--teal); outline-offset: 1px; border-color: transparent; }
@media (max-width: 720px) {
  .card-head .list-search { width: 100%; max-width: none; margin-left: 0; order: 9; }
  .card-head { flex-wrap: wrap; }
}
@media (max-width: 1150px) {
  /* tablets: tighter cells and narrower chip lists so Outlets keeps its buttons on screen */
  .list-table th, .list-table td { padding-left: 12px; padding-right: 12px; }
  .list-table .chips { max-width: 190px; }
  .list-table td:first-child { white-space: normal; min-width: 150px; }
}

/* Lists page (2026-10-11): payment types full width, the short lists in a two-column grid. */
.lists-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; margin-top: 18px; align-items: start; }
@media (max-width: 1080px) { .lists-grid { grid-template-columns: 1fr; } }
.lists-grid-wide { grid-column: 1 / -1; }

/* Users list (2026-10-11): row "More" menu, pager, aligned filter controls. */
.menu-pop { min-width: 210px; padding: 6px; display: grid; gap: 1px; text-align: left; }
.menu-pop form { margin: 0; }
.menu-item {
  display: block; width: 100%; padding: 9px 12px; border: 0; border-radius: 8px; background: none;
  font: inherit; font-size: 14px; color: var(--ink); text-align: left; text-decoration: none; cursor: pointer; white-space: nowrap;
}
.menu-item:hover { background: var(--sand); }
.pager { display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; padding: 14px 18px; border-top: 1px solid var(--line); }
.pager .is-current { background: var(--ink); color: #fff; border-color: var(--ink); cursor: default; }
/* native selects were 39px beside 43px inputs and enhanced selects */
.filters .filter-row .field > select { height: 43px; }
@media (max-width: 1150px) and (min-width: 721px) {
  /* users grid on tablets: let cells wrap (badges stack, long usernames break) so Edit/More stay on screen */
  table.cards-xs th, table.cards-xs td { padding-left: 10px; padding-right: 10px; }
  table.cards-xs td { white-space: normal; }
  table.cards-xs td .mono { word-break: break-all; }
  table.cards-xs .line-actions { flex-wrap: nowrap; }
}

/* Pinned row menus scroll inside themselves; a wheel that reaches the end must not scroll the page behind
   (2026-10-11, PMS panel on Hotels). */
.row-more > .pop.is-pinned { overscroll-behavior: contain; }

/* Role permissions (2026-10-11): one block per permission, sticky save bar. */
.perm-opts { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 4px 16px; }
.perm-opt { display: flex; gap: 10px; align-items: flex-start; padding: 8px 10px; border-radius: 8px; cursor: pointer; }
.perm-opt:hover { background: var(--sand); }
.perm-opt input { margin-top: 3px; flex: none; accent-color: var(--teal); }
.perm-opt-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.perm-opt-name { font-size: 14px; font-weight: 500; color: var(--ink); line-height: 1.35; }
.perm-opt-name .pill { margin-left: 4px; font-size: 11px; vertical-align: 1px; }
.perm-opt-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; color: var(--ink-soft); overflow-wrap: anywhere; }
.perm-opt-note { font-size: 11.5px; color: var(--gold); }
.perm-opt.is-locked { cursor: not-allowed; }
.perm-opt.is-locked .perm-opt-name { color: var(--ink-soft); }
.role-perms .card { margin-bottom: 14px; }
.perm-savebar {
  position: sticky; bottom: 0; z-index: 5; display: flex; align-items: center; justify-content: space-between; gap: 16px;
  margin-top: 4px; padding: 12px 18px; background: var(--white); border: 1px solid var(--line); border-radius: 12px;
  box-shadow: 0 -8px 24px rgba(20, 32, 46, .08); font-size: 13px;
}

/* Staff filters (2026-10-11): six filters at a 210px floor each needed ~1,350px, so Gender and the Search
   button wrapped onto a second row, and the plain selects were 39px beside 43px controls. A grid keeps every
   filter on one row from ~1150px and wraps evenly below that. (.filterbar-labelled is used only here.) */
.filterbar-labelled { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); align-items: end; gap: 12px; }
.filterbar-labelled .fb-field select,
.filterbar-labelled .fb-field input[type=search],
.filterbar-labelled .fb-field .ss { min-width: 0; width: 100%; }
.filterbar-labelled .fb-field > select { height: 43px; }
.filterbar-labelled .fb-actions .btn { height: 43px; }

/* Card-head outlet picker (Rooms, 2026-10-11): a labelled select that sits in the card head like the title. */
.card-head-picker { display: flex; align-items: center; gap: 8px; margin: 0; }
.card-head-picker label { font-size: 13px; }
.card-head-picker select { font: inherit; font-weight: 600; font-size: 15px; padding: 6px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--white); }
.card-head-picker label { margin: 0; }

/* Settings values (2026-10-11): a long value (receipt policy, recipient list) was one unbroken line — 578px past the
   overrides card, cutting off the row's own button — or a 10-line bold stack in the list. At most two lines, the
   full value on hover. Short values (15 minutes, Yes, 24 hours) look exactly as before. */
.val-clamp {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  white-space: normal; overflow-wrap: anywhere; max-width: 340px; vertical-align: middle;
}
td.num .val-clamp { margin-left: auto; }
.big-value.is-long { font-size: 15px; font-weight: 500; letter-spacing: 0; line-height: 1.5; }
table.timeline { table-layout: fixed; width: 100%; }
table.timeline th:first-child, table.timeline td:first-child { width: 34%; }
table.timeline th:nth-child(2), table.timeline th:nth-child(3) { width: 24%; }
table.timeline td:nth-child(2) { white-space: nowrap; }
table.timeline td { white-space: normal; }

/* Report filters (2026-10-11): fields were capped at 190px, so Run/Reset — and on Cash drawers the To date — wrapped
   onto a second row; the native Status select was 39px beside 43px controls. Fields now share the row. */
.rep-filter-grid { grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); }
.rep-filter .field > select { height: 43px; }
.rep-filter .field-actions { white-space: nowrap; }
.rep-filter .field > input[type=date] { height: 43px; }

/* Invoice "Money" ledger in the side card (2026-10-11): 5 columns overflowed the card by 81px. */
.ledger-table th, .ledger-table td { padding-left: 10px; padding-right: 10px; }
.ledger-table td { white-space: normal; }
.ledger-table td.num { white-space: nowrap; }
@media (max-width: 1150px) and (min-width: 721px) {
  /* invoice list on tablets: let cells wrap so Print stays on screen */
  .inv-table th, .inv-table td { padding-left: 10px; padding-right: 10px; }
  .inv-table td { white-space: normal; }
}

/* Roster toolbar (2026-10-11): outlet + day picker and "Publish this day" on one row. */
.roster-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
.roster-toolbar .filterbar, .roster-toolbar .filterbar-actions { margin: 0; }
