/* Madar base stylesheet -- docs/DESIGN_SYSTEM.md §7.
   Reset, typography (§3), the app shell (§6.2), the layout vocabulary (§6.3-6.11),
   buttons (§4), form controls, the four screen states (§5.1), focus (§10).

   Two rules hold everywhere in this file, and both are enforced by
   core/test_design_rules.py, not just asserted here:
     * Logical properties only (§1.2). Never left/right for layout or spacing.
     * Every colour from a token (§1.3). Never a hex value outside tokens.css.

   Rewritten 15 Sep 2026 for the approved design pass. There are NO SHADOWS in
   this file, on purpose (§2) -- separation is border and colour. */


/* ==========================================================================
   RESET
   ========================================================================== */

*, *::before, *::after { box-sizing: border-box; }

/* `hidden` MUST ACTUALLY HIDE, EVERYWHERE, AND THIS IS THE ONE LINE THAT DOES
   IT. The user agent's `[hidden] { display: none }` lives in the UA origin and
   loses to ANY author-origin `display`, however weak its selector -- so
   `.tl-header-btn { display: flex }` left the calendar's full-screen button
   visible on every view, including the two that never loaded the script meant
   to reveal it. The owner clicked a live-looking control and nothing happened.

   calendar.css had met this before and answered it per component, twice. A
   third element was still missed, and an independent review then found a
   FOURTH that the per-component test could not see either:
   `.choice-cards` in the manual booking form, hidden with
   `{% if not units %}hidden{% endif %}` -- the canonical Django spelling of a
   conditional boolean attribute, and the shape most likely to be written next.

   So the answer moves here, once, instead of being rediscovered per element.
   `!important` is load-bearing: without it this rule ties with every author
   `display` and loses to any that is more specific. The three per-component
   guards in calendar.css are now redundant; they are left in place with their
   own history, because that history is why this line exists. */
[hidden] { display: none !important; }

html, body, h1, h2, h3, h4, p, ul, ol, figure { margin: 0; }

ul, ol { padding-inline-start: 0; list-style: none; }

img, svg { display: block; max-inline-size: 100%; }

/* Every icon comes from partials/_icon.html, which emits class="icon" and
   carries its own width/height (§6.11). This only stops a flex row from
   squashing one -- the size itself is never set here, or a nav icon and a
   button icon would need duelling overrides. */
.icon { flex-shrink: 0; }

button, input, select, textarea { font: inherit; color: inherit; }

/* A BUTTON WITH NO BACKGROUND DECLARED IS NOT TRANSPARENT -- IT IS GREY.
   This line was missing and it shipped the defect the owner photographed:
   `.appbar__account` declares padding, radius, gap and `color: inherit` and
   never a background, so it wore the user agent's `ButtonFace`
   (rgb(240,240,240)) and a `2px outset` 3D border on EVERY screen in the
   product. In dark mode that is a white box, and the inherited `--ink` is
   #F0F5F1 on it -- text at 1.03:1, which is why the owner saw an empty one.

   The reset above only ever covered `font` and `color`, so the gap was
   silent: nothing in the stylesheet was wrong, something was absent, and a
   missing declaration is invisible to a reviewer reading the component.

   SCOPED TO `button` ONLY. Inputs, selects and textareas DO want a border by
   default -- the form vocabulary paints them deliberately -- and clearing
   theirs here would trade one invisible control for several. Every styled
   button in this product declares its own border (`.btn` carries
   `border: 1px solid transparent` precisely so a variant can colour it), so
   none of them change. Measured before and after with
   `_review/_probe_uachrome.py`: three controls wore UA chrome, now none do. */
button { background: none; border: 0; }

/* --link, not --primary: a fill and a link have different contrast
   thresholds and only one of them passed (see tokens.css). */
a { color: var(--link); }
a:hover { color: var(--link-hover); }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}


/* ==========================================================================
   TYPOGRAPHY (§3)
   Base is 14px, not 16px. This changes the density of every screen at once --
   intended, see §3.
   ========================================================================== */

body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--fs-14);
  line-height: 1.7; /* Arabic needs more leading than Latin -- cramped leading is
                       the most common way Arabic UI looks wrong (§3). */
}

h1, h2, h3, h4 { line-height: 1.3; font-weight: 700; }

h1 { font-size: var(--fs-24); letter-spacing: -0.4px; }
h2 { font-size: var(--fs-20); letter-spacing: -0.2px; }
h3 { font-size: var(--fs-17); }
h4 { font-size: var(--fs-15); }

small, .text-sm { font-size: var(--fs-13); }
.text-xs { font-size: var(--fs-12); }
.text-muted { color: var(--muted); }

/* Arabic has no italic form (§1.10) -- emphasis is weight or colour, never a
   slant. Enforced here so a stray <em> doesn't silently italicise, and again in
   core/test_design_rules.py so nobody re-adds font-style: italic elsewhere. */
em, i { font-style: normal; font-weight: 600; }

/* Numbers/dates/money/times stay Western digits and render LTR inside RTL text
   (§3). <bdi> isolates their direction; tabular-nums aligns them in a column so
   a list of prices lines up on its decimal point. */
bdi {
  unicode-bidi: isolate;
  font-variant-numeric: tabular-nums;
}


/* ==========================================================================
   FOCUS (§10)
   Every interactive element. :focus-visible, not :focus, so a mouse click
   leaves no ring. outline, not box-shadow: §2 forbids shadows, and outline
   follows the element's own border-radius.
   ========================================================================== */

:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible,
.table-row:focus-visible,
.status-chip:focus-visible,
.tab:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* The sidebar is a dark surface in BOTH themes. --focus measures 1.51:1 against
   it -- the brand green on the dark-green sidebar is very nearly the same
   colour. This is the entire reason two focus tokens exist (§10). */
.sidebar :focus-visible,
.bottom-nav__action:focus-visible {
  outline-color: var(--focus-on-dark);
}

/* Keyboard users land here first; invisible until focused. */
.skip-link {
  position: absolute;
  inset-block-start: var(--sp-8);
  inset-inline-start: var(--sp-8);
  z-index: 100;
  padding: var(--sp-8) var(--sp-16);
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-weight: 600;
  transform: translateY(-200%);
}
.skip-link:focus {
  transform: none;
}


/* ==========================================================================
   APP SHELL (§6.2)
   [ sidebar 248px -- inline-start, which is the RIGHT in RTL ] [ content ]
   ========================================================================== */

/* THE SHELL IS A COLUMN NOW: the top bar, then everything else
   (product-owner ruling, 15 Oct 2026 -- the bar spans the whole viewport and
   the sidebar starts at its bottom edge). `.app` was the ROW that held the
   sidebar beside the content column; that row is `.app__body`, one level
   down, and the bar is `.app`'s first child so nothing can sit beside it. */
.app {
  display: flex;
  flex-direction: column;
  /* `dvh` WITH A `vh` FALLBACK (device-matrix round, 15 Oct 2026).
     `100vh` on iOS Safari is the LARGE viewport -- the height the page would
     have if the browser's toolbars were hidden -- so a shell sized by it is
     taller than what the reader can actually see, and everything that hangs
     off its bottom edge hangs below the visible area. `100dvh` is the
     viewport that is actually there right now.

     The two declarations ARE the fallback and need no `@supports`: an engine
     that does not know `dvh` drops the second line and keeps the first,
     which is exactly the wanted behaviour. */
  min-block-size: 100vh;
  min-block-size: 100dvh;
  /* THE INLINE INSETS, which `viewport-fit=cover` made necessary -- see that
     meta tag's comment in templates/base.html. An iPhone in landscape puts
     ~47px of each page edge under the notch and the rounded corner. Paid
     here, once, on the shell, rather than by every screen. */
  padding-inline: var(--safe-area-start) var(--safe-area-end);
}

/* The row the sidebar, the drawer scrim and the content column live in.
   `min-block-size: 0` so a tall content column cannot push the row past the
   viewport and strand the sidebar's own foot; `flex: 1` so a SHORT page
   still paints the sidebar to the bottom of the screen, which is what
   `.app { min-block-size: 100vh }` used to do for it directly. */
.app__body {
  flex: 1;
  display: flex;
  min-block-size: 0;
}

.app__main {
  flex: 1;
  min-inline-size: 0;   /* without this a wide grid child forces the whole
                           page to scroll sideways instead of scrolling
                           inside its own wrapper */
  display: flex;
  flex-direction: column;
  background: var(--bg);
}

/* ---- Sidebar ---- */

.sidebar {
  /* POSITIONED, because the collapse handle is absolute against it
     (SHELL_SPEC §2b; the artboard's own <aside> carries `position:
     relative` for the same reason).

     MEASURED DEFECT, 8 Oct 2026: without this the handle's
     `inset-inline-end: -12px` resolved against the initial containing block
     instead, putting it 12px past the page's inline-end edge -- which in RTL
     is the LEFT -- at 1024, 1100, 1280 and 1400px. The page scrolled
     sideways by exactly 12px on every desktop screen in the product. Found
     by the screenshot probe's both-directions overflow check, which exists
     because a scrollWidth-only check reports an RTL page like this as
     clean. */
  position: relative;
  inline-size: var(--sidebar-width);
  flex-shrink: 0;
  background: var(--sidebar);
  color: var(--sidebar-text);
  padding: var(--sp-20) var(--sp-14) var(--sp-16);
  display: flex;
  flex-direction: column;
}

.wordmark {
  display: flex;
  align-items: center;
  gap: 11px;
  padding-inline: var(--sp-8);
  padding-block-end: var(--sp-22);
  text-decoration: none;
  color: inherit;
}

/* The logo mark. Real brand assets still do not exist (§7) -- until they do
   this is a 34px square with the glyph. The old gold --accent dot is gone. */
.wordmark__glyph {
  inline-size: 34px;
  block-size: 34px;
  flex-shrink: 0;
  border-radius: var(--radius-sm);
  background: var(--primary-soft);
  /* --brand-ink, the PACKAGE'S OWN value for this square (token sync
     round, 11 Oct 2026). The two values before it are worth keeping,
     because each was a real fix:

       --sidebar   #000000 on #132A1D in dark = 1.38:1. An invisible
                   glyph on every authenticated screen, in its RESTING
                   state. Caught by the painted-ground rule's first run,
                   not by the eye-check -- the owner was in light mode.
       --link      9.06:1 light / 4.80:1 dark. Visible, and wrong in a
                   quieter way: a link's colour standing in for a brand
                   mark's, so the two could never move apart.

     The package draws this chip 39 times per theme and it agrees with
     neither: #14261A on #E9F0EC and #7FD69B on #132A1D -- 13.72:1 and
     8.71:1. That is the designer's blessed category (a), and his own fix
     is a BRAND_CHIP sentinel in theme.py for the same reason this is a
     separate token. */
  color: var(--brand-ink);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: var(--fs-14);
}

.wordmark__text {
  display: flex;
  flex-direction: column;
  line-height: 1.35;
}
.wordmark__name {
  color: var(--on-dark);
  font-size: var(--fs-15);
  font-weight: 700;
}

/* TOUCH, ANY WIDTH (15 Oct 2026 device-matrix round). The brand mark is
   69.95x34px painted -- wide enough, 6px short on the block axis -- in
   BOTH the sidebar and the desktop top bar it moved into. A mouse does not
   owe 40px, so this grows the HIT AREA only, via the same negative-inset
   pseudo-element idiom `core/device_probes.py` already detects: the glyph
   and the word stay exactly as drawn, on every screen, in both themes. */
@media (pointer: coarse) {
  .wordmark { position: relative; }
  .wordmark::after {
    content: "";
    position: absolute;
    inset: -3px 0;
  }
}
/* `.wordmark__sub` AND `.sidebar__date` WERE HERE and are gone (15 Oct
   2026). The sidebar has no header: the wordmark moved into the full-width
   top bar, the Organization name became the account menu's «الحساب» row and
   the date line its «اليوم» row. Both tokens they used -- `--sidebar-dim`
   and `--sidebar-dim-2` -- are still live elsewhere in this file (the phone
   drawer's user role, and the collapse handle's own muted text), so neither
   has been orphaned by the move.

   The padding this pair contributed is NOT replaced: `.wordmark`'s
   `padding-block-end` was the gap under the brand, and the brand left with
   it. The nav is now the sidebar's first child, against the sidebar's own
   `padding-block-start`. */

/* Class names kept from the previous top nav on purpose: they still describe
   the thing (site navigation, current page), and two tests select on them. */
.site-nav {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

/* 44px rows, 10px radius, generous gaps -- SHELL_SPEC §2b, the 6 Oct
   package's restyle. `position: relative` is for the active row's 3px edge
   indicator, which is absolutely positioned against it. */
.site-nav__link {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--sp-12);
  block-size: var(--nav-item-height);
  padding-inline: var(--sp-12);
  border-radius: var(--radius);
  color: inherit;
  text-decoration: none;
  font-size: var(--fs-14);
}
.site-nav__link:hover {
  background: var(--sidebar-active);
  color: var(--on-dark);
}

/* The current page renders as a plain, non-clickable <span>, not a
   disabled-looking link -- the same convention as
   .calendar-view-switcher__btn--active.

   THE ACTIVE ROW IS A QUIET PILL: the fill is --sidebar-active, the icon
   takes the brand's light-green accent, and a 3px indicator sits on the
   SIDEBAR's own inline-start edge. The reference does this with its brand
   red; ours is the green (SHELL_SPEC §2b).

   Three signals, not one: the pill, the weight, and aria-current. The accent
   icon is the fourth and is never the only one, which is what rule 9 asks. */
.site-nav__link--active {
  background: var(--sidebar-active);
  color: var(--on-dark);
  font-weight: 600;
}
.site-nav__link--active .icon { color: var(--sidebar-accent); }

/* -14px reaches out through the nav's own 14px sidebar padding to land the
   indicator ON the sidebar's edge, which is where the artboard draws it --
   not inset by the padding. */
.site-nav__link--active::before {
  content: "";
  position: absolute;
  inset-inline-start: calc(-1 * var(--sp-14));
  inset-block-start: 8px;
  inline-size: 3px;
  block-size: 28px;
  border-radius: var(--radius-pill);
  background: var(--sidebar-accent);
}

/* The label, so the collapsed rail has one thing to hide (§2b: "labels
   become tooltips"). A span rather than a bare text node for exactly that
   reason -- a text node cannot be addressed by CSS. */
.site-nav__label {
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---- «التقارير»: the one expandable group (SHELL_SPEC §2b) ---- */

/* A plain flex column, not `display: contents`. The visual result is the
   same (the nav's own 2px gap between rows), and <details> with
   display:contents has a history of layout bugs in WebKit -- which is the
   engine a Gulf owner is most likely holding, and the one this project
   captures a second screenshot set under. */
.site-nav__group {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

/* A <summary> is display:list-item by default and draws a disclosure
   triangle; the group draws its own chevron, so the marker goes. Both
   spellings are needed -- ::-webkit-details-marker for WebKit, which is
   what a Gulf owner is most likely holding. */
.site-nav__link--summary { cursor: pointer; list-style: none; }
.site-nav__link--summary::-webkit-details-marker { display: none; }

.site-nav__chev {
  margin-inline-start: auto;
  display: flex;
  color: var(--sidebar-dim);
  transition: transform 120ms ease;
}
.site-nav__group[open] .site-nav__chev { transform: rotate(180deg); }

.site-nav__children {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  /* Indented to the width of a row's icon plus its gap, so a child's text
     starts where its parent's text starts. */
  padding-inline-start: calc(19px + var(--sp-12) + var(--sp-12));
  padding-block: var(--sp-2);
}
.site-nav__child {
  display: flex;
  align-items: center;
  /* 44px even here: these are real navigation rows and the phone drawer
     renders the same markup (§2b, and README rule 6). */
  min-block-size: var(--nav-item-height);
  padding-inline: var(--sp-10);
  border-radius: var(--radius-sm);
  color: var(--sidebar-text);
  text-decoration: none;
  font-size: var(--fs-13);
}
.site-nav__child:hover { background: var(--sidebar-active); color: var(--on-dark); }
.site-nav__child--active {
  background: var(--sidebar-active);
  color: var(--on-dark);
  font-weight: 600;
}


.site-nav__badge {
  margin-inline-start: auto;
  background: var(--sidebar-badge);
  color: var(--sidebar-text);
  font-size: var(--fs-11);
  font-weight: 700;
  padding: 1px 7px;
  border-radius: 5px;
}
.site-nav__link--active .site-nav__badge {
  background: var(--primary-soft);
  /* --sidebar here was 1.38:1 on this fill in dark mode -- the reservations
     and payments counts beside the ACTIVE nav item, which is the one an owner
     is looking at. Same ground, same foreground as the wordmark above. */
  color: var(--link);
}

.sidebar__foot {
  margin-block-start: auto;
  display: flex;
  flex-direction: column;
  gap: var(--sp-10);
}

/* `.sidebar__theme` WAS HERE and is gone (shell round, 8 Oct 2026). The
   appearance control moved to the account menu's «المظهر» row, because
   SHELL_SPEC §2b makes the sidebar navigation only, and on the phone to the
   drawer's own value row. Its measured note is kept where it is still true --
   the drawer's logout row faced the same problem on the same ground and
   `--danger-on-dark` records the answer.

   NOT a reversal of the 6 Oct ruling that put a switch here: that ruling was
   about the CAPABILITY, and the capability is larger now -- three states
   (فاتح / داكن / تلقائي) where this switch had two. See the long note in
   templates/base.html's sidebar foot.

   `.sidebar__user` (the BLOCK) is gone with it -- the top bar's account
   control replaced it. Its three TEXT classes below survive, because the
   phone drawer's own tappable user block still uses them. */

.sidebar__user-text {
  display: flex;
  flex-direction: column;
  line-height: 1.35;
  min-inline-size: 0;
}
.sidebar__user-name {
  color: var(--on-dark);
  font-size: var(--fs-13);
  font-weight: 600;
}
.sidebar__user-role {
  font-size: var(--fs-11);
  color: var(--sidebar-dim);
}

.sidebar__avatar {
  inline-size: 32px;
  block-size: 32px;
  flex-shrink: 0;
  border-radius: var(--radius-sm);
  background: var(--avatar-bg);
  color: var(--avatar-text);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: var(--fs-13);
}

/* `.theme-toggle` WAS HERE and is gone (S4, product-owner ruling 19 Sep 2026:
   "the sidebar footer exactly as drawn"). Its own comment said the placement
   was ours and that no desktop artboard draws one. See templates/base.html's
   tombstone in the sidebar foot for the consequence -- the product now has no
   theme control at all, and 05-OPEN_DECISIONS wants one on the More screen,
   which is unbuilt. */

/* «موقع الحجز» -- 01 §5's eighth nav item, drawn by every desktop artboard,
   with no route behind it (Phase E). A VISIBLE, DISABLED row: the product's
   own treatment for a drawn-but-unbuilt control, the same one
   `calendar_menu.html`'s two «قريبًا» rows use. Not an <a>, so there is no
   link to nowhere; §1.9's reason-in-plain-text is the chip beside it, never a
   tooltip. */
.site-nav__link--disabled {
  color: var(--sidebar-dim);
  cursor: default;
}

.site-nav__soon {
  margin-inline-start: auto;
  padding: 1px 7px;
  border-radius: 5px;
  background: var(--sidebar-badge);
  color: var(--sidebar-text);
  font-size: var(--fs-11);
  font-weight: 600;
}


/* ==========================================================================
   CONTENT AREA (§6.2)
   There is no reading-width cap any more -- the shell replaced it. The old
   1200px .content-column and its --wide escape hatch are retired; both class
   names are kept as the page container so the two Calendar templates, which
   set {% block main_class %} themselves, keep working untouched until Phase B
   rebuilds them (§6.2).
   ========================================================================== */

.content-column,
.page {
  padding: var(--sp-26) var(--sp-32);
}

/* Retired, kept as a no-op so an existing template's class list still parses
   to something real. Do not use it on a new screen. */
.content-column--wide { }

@media (max-width: 1023px) {
  .content-column,
  .page { padding: var(--sp-16); }
}

.form-column {
  max-inline-size: 680px;
}

.stack > * + * { margin-block-start: var(--sp-16); }


/* ==========================================================================
   CARD (§6.1, §6.6)
   ========================================================================== */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.card__header {
  display: flex;
  align-items: center;
  gap: var(--sp-10);
  padding: var(--sp-12) var(--sp-18);
  border-block-end: 1px solid var(--line);
}
.card__title { font-weight: 700; }
.card__hint { color: var(--muted); font-size: var(--fs-12); }

.card__body { padding: var(--sp-18); }


/* ==========================================================================
   PAGE HEADER (§6.3)
   Title column, then margin-inline-start:auto, then the actions.
   One primary button per screen (§1.8).
   ========================================================================== */

.page-header {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-16);
  padding-block-end: var(--sp-18);
  flex-wrap: wrap;
}

.page-header__text {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.page-header__title {
  margin: 0;
  font-size: var(--fs-24);
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -0.4px;
}

.page-header__desc {
  color: var(--muted-warm);
  font-size: var(--fs-13);
}

.page-header__actions {
  margin-inline-start: auto;
  display: flex;
  align-items: center;
  /* The row hugs its content in the ordinary case, so this changes nothing
     there. It matters when a WRAPPED item makes the box wider than the
     controls -- the disabled-filter reason on Year and Agenda -- where the
     default `flex-start` left the icon row stranded in the middle of the
     header instead of at the inline-end where every artboard draws it. */
  justify-content: flex-end;
  gap: var(--sp-10);
  flex-wrap: wrap;
}


/* ==========================================================================
   KPI CARD (§6.4)
   A number that needs attention colours the NUMBER and the card's BORDER --
   never the card's fill.
   ========================================================================== */

.kpi-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--sp-14);
  padding-block-end: var(--sp-18);
}

.kpi-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--sp-16) var(--sp-18);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.kpi-card__value {
  font-size: var(--fs-26);
  font-weight: 700;
  line-height: 1.2;
}

.kpi-card__label {
  color: var(--muted-warm);
  font-size: var(--fs-12);
}

/* A KPI THAT GOES SOMEWHERE. Added 17 Sep 2026, because the Dashboard's
   "occupied units right now" was a count the owner could not get to -- half a
   feature -- and the build agent correctly reported the gap rather than
   hacking a bare <a> around a card with no link treatment.

   THE WHOLE CARD IS THE TARGET, not a link inside it. A 44px control tucked
   into a card is a smaller target than the card it sits in, and on a phone the
   owner taps the number. Same reasoning §6.6 already applies to table rows.

   A chevron marks it as a destination: colour alone never carries meaning
   (§1), and without it a clickable card and a static one are identical until
   you try. */
.kpi-card--link {
  text-decoration: none;
  color: inherit;
  /* Already flex-column with 2px gaps; the chevron needs its own row that
     does not disturb the value/label rhythm. */
  position: relative;
  min-block-size: var(--control-height);
}

.kpi-card--link:hover { background: var(--surface-2); }

.kpi-card--link:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.kpi-card__go {
  position: absolute;
  inset-block-start: var(--sp-14);
  /* inset-inline-END: the chevron sits at the reading end of the card, which
     is the LEFT in RTL. A physical `right` here would put it under the number
     in Arabic. */
  inset-inline-end: var(--sp-14);
  color: var(--chevron);
  display: inline-flex;
}

.kpi-card--warning { border-color: var(--warning-border); }
.kpi-card--warning .kpi-card__value { color: var(--warning-text); }
.kpi-card--success .kpi-card__value { color: var(--success-text); }
.kpi-card--danger  { border-color: var(--danger-border); }
.kpi-card--danger .kpi-card__value { color: var(--danger-text); }


/* ==========================================================================
   TOOLBAR (§6.5)
   ========================================================================== */

.toolbar {
  display: flex;
  align-items: center;
  gap: var(--sp-12);
  padding: var(--sp-14) var(--sp-18);
  flex-wrap: wrap;
}

.toolbar__count {
  margin-inline-start: auto;
  color: var(--muted);
  font-size: var(--fs-12);
}

.search-field {
  display: flex;
  align-items: center;
  gap: 9px;
  block-size: 38px;
  inline-size: 300px;
  max-inline-size: 100%;
  padding-inline: var(--sp-12);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--muted);
}
/* OWNER RULING, 7 Oct 2026: THE SEARCH FIELD IS ONE CONTROL -- the icon lives
   INSIDE the single bordered field, not in a box beside it. This supersedes
   the artboard, and is recorded in docs/HANDOFF.md §8 like ruling J.

   It was already written that way and did not render that way, which is the
   part worth keeping. `input[type="text"]` below is specificity (0,1,1) and
   beats a bare `.search-field input` (0,1,1) on source order, so the generic
   field styling -- a 1px border, a --bg fill, a radius and a 44px height --
   was repainting a SECOND bordered box inside the first one, with the icon
   stranded outside it. Confirmed by walking the computed styles:
   `input.cal-toolbar-search-input` reported `border 1px, radius 8px,
   background rgb(250,250,248)` inside a field that had already drawn its own.

   This is the same shape as the selected-chip defect of 6 Oct: a generic rule
   out-specifying the component it sits inside. The selectors below are
   attribute-qualified so they win on specificity (0,2,1) rather than on which
   file loaded last. */
.search-field input,
.search-field input[type="text"],
.search-field input[type="search"] {
  border: 0;
  background: transparent;
  padding: 0;
  block-size: auto;
  inline-size: 100%;
  font-size: var(--fs-13);
  color: var(--ink);
}
.search-field input:focus-visible { outline: none; }
.search-field:focus-within {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* Segmented tabs: a --chip container with 4px padding, the active tab --ink. */
.tabs {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-4);
  background: var(--chip);
  border-radius: 9px;
  flex-wrap: wrap;
}

.tab {
  block-size: 32px;
  display: flex;
  align-items: center;
  padding-inline: var(--sp-14);
  border-radius: 7px;
  color: var(--muted-2);
  font-size: var(--fs-13);
  text-decoration: none;
}
.tab:hover { color: var(--ink); }

/* ...BUT NEVER AT THE COST OF A MODIFIER'S OWN PAIR. `.tab:hover` is one
   specificity step above a bare modifier class, so it was repainting the
   ACTIVE tab's label to --ink -- on --inverse-surface, which is the same
   colour. Measured in a headless browser against these stylesheets: light
   rgb(22,33,26) on rgb(22,33,26), dark rgb(240,245,241) on rgb(240,245,241),
   1.00:1 in both. The owner found it on the reservations list and the units
   page, and it was invisible from here because every piece was individually
   correct.

   The pointer rests on the chip you just clicked, so this was not an edge
   case -- it was the normal state of the selected filter.
   core/test_design_rules.py::test_a_painted_ground_never_wins_against_its_own_foreground
   now fails the build on the shape rather than on this instance; it is the
   second time this exact pairing has shipped (the dark-mode active tab at
   1.00:1, 16 Sep 2026, is why --on-inverse exists at all).

   The tone tabs get the same treatment for a milder reason: hovering them
   was legible but dropped their tone, so the colour stopped carrying its
   meaning for as long as the pointer was on it. The disabled tab needs it
   because brightening to --ink on hover reads as "this is live". */
.tab--active:hover,
.tab--active:focus-visible { color: var(--on-inverse); }
.tab--alert:hover,
.tab--alert:focus-visible { color: var(--danger-text); }
.tab--warn:hover,
.tab--warn:focus-visible { color: var(--warning-text); }
.tab--disabled:hover,
.tab--disabled:focus-visible { color: var(--faint); }
/* `.tab--desktop-only` LIVED HERE FOR PART OF ONE DAY and is deleted (17 Sep
   2026). It hid the calendar's timeline tab below 640px while that view was
   ruled desktop-only; the ruling is retired -- every calendar view is now a
   grid that scrolls inside its own container at every width -- so there is no
   longer a tab that leads somewhere unrenderable. Recorded rather than silently
   removed, because the class name will otherwise look like something that was
   lost. */

/* The escalation tab -- BR-032's overdue-verification filter. Danger tones
   because it is the one tab that means "something here needs you", and it is
   rendered only when the count is non-zero: a tab permanently reading «0» is
   furniture, and an owner learns not to look at furniture. */
.tab--alert {
  background: var(--danger-bg);
  color: var(--danger-text);
  font-weight: 600;
}

/* The SECOND escalation tab -- the Dashboard's «تجاوزت موعد المغادرة» count
   lands here as `?overstay=1`. Warning tones, not danger: an overstay is an
   exception to reconcile (a check-out nobody recorded), not an SLA breach.
   It is deliberately the same tone as the KPI card that links here
   (.kpi-card--warning) -- one fact, one colour, across two screens. Same
   rendered-only-when-non-zero rule as .tab--alert above. */
.tab--warn {
  background: var(--warning-bg);
  color: var(--warning-text);
  font-weight: 600;
}

/* A tab for a view that EXISTS BUT IS NOT BUILT YET (the calendar package's
   switcher ships all four tabs; Year and Agenda arrive later in the same
   run). Shown disabled rather than hidden -- a switcher missing half its
   tabs teaches the wrong shape -- and per the package's rule 7 the REASON is
   plain text beside the switcher (.tabs-note below), never a tooltip. */
.tab--disabled {
  color: var(--faint);
  cursor: default;
}

/* The plain-text reason beside a .tab--disabled. */
.tabs-note {
  font-size: var(--fs-11);
  color: var(--muted);
}

.tab--active {
  background: var(--inverse-surface);
  color: var(--on-inverse);
  font-weight: 600;
}


/* ==========================================================================
   TABLE (§6.6)
   Semantic <table> markup, NOT the artboards' <div> grid -- so screen-reader
   table semantics and .data-table--stack's mobile treatment both survive
   (§6.6, §11). .data-table--grid opts a table into the proportional grid row
   via a per-screen --cols custom property.
   ========================================================================== */

/* A TABLE WIDER THAN ITS CARD SCROLLS INSIDE THIS BOX -- the page never scrolls
   sideways. `overflow-x: auto` alone does NOT achieve that in RTL, which is the
   whole reason `contain: paint` is here and is not decoration.

   MEASURED, at 1024px, on /payments/ (eight columns whose min-content sums to
   907px inside a 710px card):

     baseline                     doc.scrollWidth 1163   page moved 138px
     wrapper overflow-x: hidden   doc.scrollWidth 1163   page moved 138px
     card overflow: clip          doc.scrollWidth 1163   page moved 138px
     body overflow-x: hidden      doc.scrollWidth 1163   page moved 138px
     wrapper contain: paint       doc.scrollWidth 1024   page CANNOT move

   In RTL the table overflows off the LEFT, and Chromium hands that scrollable
   overflow to the DOCUMENT even though this element is a scroll container --
   the sidebar itself slid 138px. `contain: paint` is what keeps the overflow
   in the box it belongs to. The table still scrolls inside the wrapper, which
   is the design.

   Found by the five-width probe in scripts/screenshots.py on its first run,
   at exactly the width band that probe exists for. Both anchor widths (375,
   1400) called this page clean. */
/* AND THE PRICE IT CHARGES, written down 14 Oct 2026 after it was removed
   and put back within the hour.

   `contain: paint` also makes this element a CONTAINING BLOCK for
   `position: fixed` descendants, and it CLIPS them. So a popover opened from
   inside this wrapper cannot escape it by going fixed: the guests list's row
   menu, anchored to viewport coordinates of `top: 685px`, painted at y=-304
   -- three hundred pixels above the screen -- because the wrapper
   reinterpreted them as its own. Before that anchor existed the same menu
   was simply cut to 9px of its 142px on the last row.

   DELETING THIS LINE FIXES THE MENU AND BREAKS THE PAGE. It was tried: the
   menu came out perfect at both widths and /payments/ went straight back to
   sliding 138px sideways at 1024px, which is the defect the measurement
   above exists to prevent and which no anchor-width capture would have
   caught. The four alternatives in that table were already tried once.

   So a popover that must leave this box LEAVES THE DOM, not the stacking
   context: `x-teleport` to <body>, which is what the guests list's row menu
   does. Its own comment carries the htmx caveat that comes with it. */
.data-table-wrapper {
  overflow-x: auto;
  contain: paint;
}

.data-table {
  inline-size: 100%;
  border-collapse: collapse;
}

.data-table th {
  font-size: var(--fs-11);
  font-weight: 600;
  letter-spacing: 0.5px;
  color: var(--muted);
  padding: var(--sp-10) var(--sp-18);
  text-align: start;
  border-block: 1px solid var(--line);
}

.data-table td {
  padding: var(--sp-12) var(--sp-18);
  text-align: start;
  border-block-end: 1px solid var(--row);
}

.data-table tbody tr:hover { background: var(--bg); }

/* §6.6's `12px 18px` is a DESKTOP row padding, measured from a 1440px
   artboard. Applied unconditionally it costs 36px of horizontal padding per
   column, which is what tipped Reservation Details' 5-column line-items table
   31px past its container at 375px -- a real regression this pass introduced,
   caught by measuring scrollWidth, not by looking at the screenshot. 12px
   inline is the value those tables had before and fits. */
@media (max-width: 1023px) {
  .data-table th,
  .data-table td { padding-inline: var(--sp-12); }
}

.data-table td.actions {
  text-align: end;
  white-space: nowrap;
}

/* Proportional columns plus a 40px arrow column, per §6.6. A screen supplies
   --cols, e.g. style="--cols: 2.3fr 1.3fr 1.5fr 1fr 1.5fr 40px". */
.data-table--grid thead tr,
.data-table--grid tbody tr {
  display: grid;
  grid-template-columns: var(--cols);
  gap: var(--sp-16);
  align-items: center;
}
.data-table--grid th,
.data-table--grid td { border-block: 0; }
.data-table--grid thead tr { border-block: 1px solid var(--line); }
.data-table--grid tbody tr { border-block-end: 1px solid var(--row); }

/* The identity cell: 32px round avatar + name/second line (§6.6). */
.identity {
  display: flex;
  align-items: center;
  gap: 11px;
  min-inline-size: 0;
}
.identity__avatar {
  inline-size: 32px;
  block-size: 32px;
  flex-shrink: 0;
  border-radius: var(--radius-pill);
  background: var(--avatar-bg);
  color: var(--avatar-text);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-12);
  font-weight: 700;
}
.identity__text {
  display: flex;
  flex-direction: column;
  line-height: 1.4;
  min-inline-size: 0;
}
.identity__name { font-weight: 600; }
.identity__sub { color: var(--muted); font-size: var(--fs-12); }

/* The whole row is the target -- never a scatter of per-row icon buttons. */
.row-chevron,
.row-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--control-height);   /* 44px touch target even though the
                                           glyph inside it is 17px (§1.7) */
  block-size: var(--control-height);
  color: var(--chevron);
}
.row-link:hover { color: var(--ink); }

/* Visible only to assistive technology. Used for a column header that is an
   icon, and for the skeleton's "loading" announcement. */
.visually-hidden {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Opt-in modifier for a table with too many columns of real-length content to
   survive at a phone width even inside .data-table-wrapper's horizontal
   scroll -- measured directly on the Reservations list (6 columns, real
   property/unit names and date ranges): 465px of content in a 343px column,
   which left the row's own "View" link scrolled out of sight by default. A
   horizontal-scroll affordance a chalet owner has to discover is not "works on
   a mobile viewport" for the exact row whose only job is getting them to that
   reservation. Below, the table becomes a stack of label:value rows per record
   -- every column still shown, nothing needing a horizontal gesture. */
@media (max-width: 640px) {
  .data-table--stack,
  .data-table--stack thead,
  .data-table--stack tbody,
  .data-table--stack tr,
  .data-table--stack th,
  .data-table--stack td {
    display: block;
  }

  /* A table carrying BOTH modifiers needs this, and it is not optional.
     `.data-table--grid tbody tr` is specificity (0,1,2); `.data-table--stack tr`
     above is (0,1,1) — and a media query adds NO specificity. So on a table with
     both classes the grid rule wins at every viewport and the rows stay a
     6-track grid inside a ~350px phone, while the rest of this block still
     applies: the header is hidden and ::before labels are injected. The result
     is a hybrid nobody designed, with every cell wrapping one character per
     line. Measured on the Reservations list: 7210 CSS px of page for 15 rows,
     against ~200px per card when this works.
     Two classes, (0,2,2), so it wins cleanly. */
  .data-table--stack.data-table--grid thead tr,
  .data-table--stack.data-table--grid tbody tr {
    display: block;
    grid-template-columns: none;
  }
  .data-table--stack thead {
    /* Column headers become each row's own data-label below instead. */
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
  }
  .data-table--stack tbody tr {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--sp-12);
    margin-block-end: var(--sp-12);
  }
  .data-table--stack td {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--sp-12);
    border-block-end: none;
    padding: var(--sp-6) 0;
    /* An email address or other unbroken Latin token has no space for the
       browser's normal wrap points to use -- without this it silently forces
       the row (and the whole stacked card) wider than the viewport. */
    overflow-wrap: anywhere;
  }
  .data-table--stack td::before {
    content: attr(data-label);
    font-weight: 600;
    color: var(--muted);
    flex-shrink: 0;
  }
  .data-table--stack td.actions { justify-content: flex-end; }
  .data-table--stack td.actions::before { content: none; }
}

/* "N-M of T" at one end, 30px page buttons at the other (§6.6). */
.pagination {
  display: flex;
  align-items: center;
  /* "N-M of T" at one end, the buttons at the other (§6.6). Without this the
     range and every button are flat flex siblings and bunch at the inline-start
     edge -- the split was described in this comment and never implemented. */
  gap: var(--sp-12);
  padding: var(--sp-12) var(--sp-18);
  /* Main.dc.html paints no background here, but 01 §1's colour table assigns
     #FCFCFA to "pagination footer" explicitly. Where 01's table and 01's own
     artboard disagree about a COLOUR, the table wins. */
  background: var(--surface-2);
  border-block-start: 1px solid var(--line);
  font-size: var(--fs-12);
  color: var(--muted);
  flex-wrap: wrap;
}
.pagination > :first-child { margin-inline-end: auto; }

.pagination__btn {
  min-inline-size: 30px;
  block-size: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding-inline: var(--sp-8);
  border-radius: 7px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--ink);
  text-decoration: none;
}
/* Prev/next only. §6.6's 30px is measured off the artboard's numbered
   buttons; §1.7's 44px minimum is a rule, and these two are the pagination
   control a thumb aims for. Numbers stay 30px -- a miss there costs one wrong
   page, not a failed interaction. */
.pagination__btn--wide {
  block-size: var(--control-height);
  padding-inline: var(--sp-14);
}

.pagination__btn--active {
  background: var(--inverse-surface);
  color: var(--on-inverse);
  border-color: var(--inverse-surface);
}

/* TOUCH, ANY WIDTH (15 Oct 2026 device-matrix round). The comment above is
   right that the numbered buttons should stay 30px -- that is the
   artboard's density call for a mouse, and a miss there costs one wrong
   page. It is not a call about a thumb: the owner's 40px tap-target ruling
   is newer and scoped to touch, and a touchscreen reaches these at ANY
   viewport width (ipad_landscape is 1194px and still a thumb). So the
   PAINT stays exactly 30px and only the HIT AREA grows, via the same
   negative-inset pseudo-element idiom `core/device_probes.py` already
   detects. `.pagination__btn--wide`'s own 44px (prev/next) is untouched --
   already over the floor. */
@media (pointer: coarse) {
  .pagination__btn { position: relative; }
  .pagination__btn::after {
    content: "";
    position: absolute;
    /* -6px, NOT -5 (measured). The containing block for an absolutely
       positioned pseudo is the ancestor's PADDING edge, one px inside its
       1px border -- so `inset: -5px` protrudes only 4px past the border
       edge per side (measured 38x38 from a 30px box, not 40x40). -6px
       accounts for the border this control actually has. */
    inset: -6px;
  }
}


/* ==========================================================================
   STATUS PILL (§6.6) and TAG
   A pill is never colour alone: it always carries a dot AND a word (§1.10).
   Tones are semantic -- ok / warn / info / danger / neutral. The mapping from
   a model's status to a tone lives in Python, never in a template (§7).
   ========================================================================== */

.status-pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 3px var(--sp-10);
  border-radius: var(--radius-xs);
  font-size: var(--fs-12);
  font-weight: 600;
  border: 1px solid transparent;
  /* NOT `white-space: nowrap`. A pill is designed for a word or two, and with
     nowrap a long one cannot wrap and simply leaves the viewport -- measured on
     Property Configuration at 375px, where a Unit's needs-attention status is a
     full sentence ("تحتاج إلى مراجعة — لا توجد فترات معرَّفة…") and overflowed
     by 18px off the inline-start edge.
     Wrapping is the lesser evil, but the real answer for that case is that a
     sentence is not a pill: pill for the state, sentence beside it. Recorded
     rather than fixed here, because the screen owns that choice. */
  max-inline-size: 100%;
}

.status-pill__dot {
  inline-size: 6px;
  block-size: 6px;
  border-radius: var(--radius-pill);
  flex-shrink: 0;
}

.status-pill--ok      { background: var(--success-bg); color: var(--success-text); border-color: var(--success-border); }
.status-pill--ok      .status-pill__dot { background: var(--success-dot); }
.status-pill--warn    { background: var(--warning-bg); color: var(--warning-text); border-color: var(--warning-border); }
.status-pill--warn    .status-pill__dot { background: var(--warning-dot); }
.status-pill--info    { background: var(--info-bg);    color: var(--info-text);    border-color: var(--info-border); }
.status-pill--info    .status-pill__dot { background: var(--info-dot); }
.status-pill--danger  { background: var(--danger-bg);  color: var(--danger-text);  border-color: var(--danger-border); }
.status-pill--danger  .status-pill__dot { background: var(--danger-dot); }
.status-pill--neutral { background: var(--neutral-bg); color: var(--neutral-text); }
.status-pill--neutral .status-pill__dot { background: var(--neutral-dot); }

.tag {
  display: inline-flex;
  align-items: center;
  padding: 3px var(--sp-10);
  border-radius: var(--radius-xs);
  background: var(--tag-bg);
  color: var(--tag-text);
  font-size: var(--fs-12);
  font-weight: 600;
}

/* The one tag that means "this needs you": money the guest says they sent,
   sitting unverified. Warning tones rather than the tag's warm neutral,
   because it is the only tag on the screen that is an ASK. Added 17 Sep 2026
   -- the payment tag was a binary answering a question with three answers,
   and this was the missing one. */
.tag--warning {
  background: var(--warning-bg);
  color: var(--warning-text);
}

/* ---- .status-badge: the pre-existing component, retained ----
   Reservation Details and the Reservations list render
   .status-badge--{{ status|lower }} for all 8 Reservation.status values, at
   their real granularity -- unlike Calendar's 4-state display, which
   deliberately compresses CONFIRMED/CHECKED_IN/CHECKED_OUT into one bucket
   (docs/CALENDAR.md §4). Re-pointed at the new status tokens here rather than
   replaced, because Phase A builds no screens: the templates that use it are
   rebuilt in Phase B, and that is when they move to .status-pill. */
.status-badge {
  display: inline-flex;
  align-items: center;
  padding: 3px var(--sp-10);
  border-radius: 6px;
  font-size: var(--fs-12);
  font-weight: 600;
  border: 1px solid transparent;
}
.status-badge--draft                { background: var(--neutral-bg); color: var(--neutral-text); }
.status-badge--hold                 { background: var(--warning-bg); color: var(--warning-text); border-color: var(--warning-border); }
.status-badge--pending_confirmation { background: var(--info-bg);    color: var(--info-text);    border-color: var(--info-border); }
.status-badge--confirmed            { background: var(--success-bg); color: var(--success-text); border-color: var(--success-border); }
.status-badge--checked_in           { background: var(--success-bg); color: var(--success-text); border-color: var(--success-border); }
.status-badge--checked_out          { background: var(--neutral-bg); color: var(--neutral-text); }
.status-badge--cancelled            { background: var(--danger-bg);  color: var(--danger-text);  border-color: var(--danger-border); }
.status-badge--expired              { background: var(--neutral-bg); color: var(--neutral-text); }
/* Payment.status reuses the same component. */
.status-badge--not_recorded         { background: var(--neutral-bg); color: var(--neutral-text); }
.status-badge--pending_verification { background: var(--warning-bg); color: var(--warning-text); border-color: var(--warning-border); }
.status-badge--verified             { background: var(--success-bg); color: var(--success-text); border-color: var(--success-border); }
.status-badge--rejected             { background: var(--danger-bg);  color: var(--danger-text);  border-color: var(--danger-border); }
.status-badge--refunded             { background: var(--info-bg);    color: var(--info-text);    border-color: var(--info-border); }
/* Property Configuration's Units list (UX/UI PRD §13's "Needs Attention"). */
.status-badge--needs-attention      { background: var(--warning-bg); color: var(--warning-text); border-color: var(--warning-border); }
.status-badge--ok                   { background: var(--success-bg); color: var(--success-text); border-color: var(--success-border); }

/* Status filter chips (Reservations list). No per-chip count, deliberately: a
   count would need to either respect the search box or not, and there is no way
   for the owner to tell which -- a number that might be lying is worse than no
   number (product-owner decision). */
.status-chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-8);
}

.status-chip {
  display: inline-flex;
  align-items: center;
  block-size: 32px;
  padding-inline: var(--sp-14);
  border-radius: var(--radius-pill);
  font-size: var(--fs-13);
  font-weight: 600;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--muted-2);
  text-decoration: none;
}
.status-chip:hover { color: var(--ink); }
/* The same defect as .tab--active above, same measurement, same fix -- this
   row and the tab group are two components built from one idea, so a fix to
   one that skipped the other would have left the owner's bug half-closed. */
.status-chip--active:hover,
.status-chip--active:focus-visible { color: var(--on-inverse); }
.status-chip--active {
  background: var(--inverse-surface);
  color: var(--on-inverse);
  border-color: var(--inverse-surface);
}


/* ==========================================================================
   BUTTONS (§4) -- five types, 44px tall, no shadow
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-8);
  block-size: var(--control-height);
  min-block-size: var(--control-height);  /* the 44px touch target, desktop too */
  padding-inline: var(--sp-18);
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  font-size: var(--fs-14);
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
}

.btn-primary {
  background: var(--primary);
  color: var(--on-primary);
}
.btn-primary:hover { background: var(--primary-hover); color: var(--on-primary); }

/* Secondary: a --chip-2 fill with NO border (§4). */
.btn-secondary {
  background: var(--chip-2);
  color: var(--ink);
}
.btn-secondary:hover { background: var(--chip); color: var(--ink); }

/* Secondary outlined: the same role inside a card, where --chip-2 on
   --surface is too faint to read as a control. */
.btn-outline {
  background: var(--surface);
  border-color: var(--border);
  color: var(--ink);
}
.btn-outline:hover { background: var(--bg); color: var(--ink); }

.btn-danger {
  background: var(--danger-bg);
  color: var(--danger-text);
}
.btn-danger:hover { background: var(--danger-bg); color: var(--danger-text); }

.btn-danger-outline {
  background: var(--surface);
  border-color: var(--danger-border);
  color: var(--danger-text);
}

/* A disabled control always carries its reason as adjacent text, never a
   tooltip -- this product is mobile-first and there is no hover (§1.9, §4). */
.btn:disabled,
.btn[aria-disabled="true"] {
  background: var(--chip-2);
  color: var(--chevron);
  border-color: transparent;
  cursor: not-allowed;
}

.btn__icon {
  inline-size: 17px;
  block-size: 17px;
  flex-shrink: 0;
}

/* `.disabled-reason` lived here for the written reason beside a disabled
   control. Its last user was the Year/Agenda filter, removed 23 Sep 2026 with
   the control itself. Deleted rather than kept "in case": an unused class is
   one a future screen copies without reading why it existed. §1.9 still
   stands -- a control that ships disabled needs its reason -- and the rule
   comes back with the next one that does. */


/* ==========================================================================
   FORM CONTROLS (§6.7)
   Fields are recessed: --bg fill on a --surface card.
   ========================================================================== */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--sp-6);
  margin-block-end: var(--sp-16);
}

.field label {
  color: var(--muted-2);
  font-size: var(--fs-13);
}

.field .hint {
  color: var(--muted);
  font-size: var(--fs-12);
}

/* ==========================================================================
   PHASE C VOCABULARY (§6.16) -- written BEFORE the screens, because
   core/test_design_rules.py fails the build on a class no stylesheet defines
   and a build agent may not edit this file. Every screen in that phase
   assembles from what is here.
   ========================================================================== */

/* --- the Dashboard's two-column body -------------------------------------
   The KPI row is the existing §6.4 component and is NOT redefined here. This
   is only what sits under it: the day's work on one side, the queue that
   needs the owner on the other. Collapses to one column below 1024px with the
   ACTION side first -- on a phone the thing that needs you outranks the thing
   that merely describes today. */
/* THE WIDE TRACK CARRIES WHAT THE OWNER MUST ACT ON. `.dash-grid__actions` is
   first in the DOM (see core/templates/core/dashboard.html), so the 1.4fr track
   is the action queue and the 1fr track is today's arrivals/departures --
   reversed on 17 Sep 2026 by product-owner decision, after the queue's status
   pill wrapped to three lines in the narrow column while the card beside it,
   empty on most days, had the room. */
.dash-grid {
  display: grid;
  grid-template-columns: 1.4fr 1fr;
  gap: var(--sp-16);
  align-items: start;
}

/* ONE COLUMN BELOW 1200px, not below 1024px like the rest of the shell's
   breakpoints. MEASURED, not chosen: the sidebar takes 248px, so a 1024-1100px
   window leaves the two cards about 400-450px each, and at that width the
   queue's «متأخر عن التحقق» pill wraps to two or three lines and every row in
   the card grows with it (117-137px against 69px at 1400px). A laptop in that
   band is a real machine, and it is the width neither `scripts/screenshots.py`
   viewport looks at. */
@media (max-width: 1199px) {
  .dash-grid { grid-template-columns: minmax(0, 1fr); }
}

/* --- a hub: a screen whose content is a set of destinations ---------------
   Settings and Reports are both "choose where to go" screens. NOT
   .choice-cards, which is a form control for mutually exclusive OPTIONS --
   these are links, and giving them the selected/unselected affordance of a
   radio would promise a choice that is not being made. */
.hub-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: var(--sp-14);
}

.hub-card {
  display: flex;
  flex-direction: column;
  gap: var(--sp-6);
  padding: var(--sp-18);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  text-decoration: none;
  color: inherit;
}
.hub-card:hover { background: var(--surface-2); }

.hub-card__title {
  font-size: var(--fs-15);
  font-weight: 600;
  color: var(--ink);
}

.hub-card__desc {
  color: var(--muted-warm);
  font-size: var(--fs-13);
  line-height: 1.75;
}

/* A hub entry for something that is not built. Rendered ONLY where the
   alternative is a reader assuming a report exists -- §1.9's rule is that a
   control the user cannot use is hidden, and a hub's whole job is to be an
   inventory, so an absence stated in words is the honest form here. It is not
   a link and has no href. */
.hub-card--soon {
  background: var(--surface-2);
  border-style: dashed;
}

.hub-card__soon {
  align-self: flex-start;
  padding: var(--sp-2) var(--sp-10);
  border-radius: var(--radius-pill);
  background: var(--neutral-bg);
  color: var(--neutral-text);
  font-size: var(--fs-12);
  font-weight: 600;
}

/* --- a horizontal bar chart, in CSS, with no library ---------------------
   The revenue report compares months. A bar length is a GRAPHIC carrying
   meaning, so §1 requires it never be the only carrier: every row shows its
   real number as text beside the bar, and the chart is readable with the bars
   removed entirely. `--bar` is the one inline custom property a template may
   set here -- a percentage, never a colour -- joining --split, --cols and
   --choice-cols on core/test_design_rules.py's whitelist. */
.bar-chart {
  display: flex;
  flex-direction: column;
  gap: var(--sp-8);
}

.bar-chart__row {
  display: grid;
  grid-template-columns: 104px 1fr 120px;
  align-items: center;
  gap: var(--sp-12);
}

.bar-chart__label {
  color: var(--muted-warm);
  font-size: var(--fs-13);
}

.bar-chart__track {
  block-size: 20px;
  border-radius: var(--radius-xs);
  background: var(--row);
  overflow: hidden;
}

.bar-chart__bar {
  /* display:block, and it is not optional. This is a <span> inside a track
     that is not a flex or grid container, so it was laid out INLINE -- and an
     inline box ignores both block-size and inline-size. Measured in Chromium
     on 17 Sep 2026: every bar on the Revenue report, including the 100% one,
     was 0.0 x 0.0 px. No bar had ever rendered on that screen.
     It survived review because §6.16 requires each row to print its real
     number as text beside the bar, so the chart read as correct with its only
     graphic missing -- the rule that makes the screen accessible is the same
     rule that hid the bug. */
  display: block;
  block-size: 100%;
  inline-size: var(--bar, 0%);
  background: var(--primary);
  border-radius: var(--radius-xs);
}

.bar-chart__value {
  text-align: end;
  font-weight: 600;
  font-size: var(--fs-13);
}

/* The empty month. A zero-height bar and a blank row look identical to a
   rendering fault, and a revenue report with a silent gap is the one chart
   nobody should have to interpret. */
.bar-chart__row--empty .bar-chart__value { color: var(--muted); font-weight: 400; }

@media (max-width: 1023px) {
  /* .dash-grid is already one column from 1199px down (above); nothing to do
     here. The `.dash-grid__actions { order: -1 }` rule that used to live here
     is gone: the queue is first in the source at every width, so "what needs
     you outranks what describes today" is one fact in one place instead of a
     desktop order plus a mobile correction. */
  .bar-chart__row { grid-template-columns: 84px 1fr 96px; gap: var(--sp-8); }
}

/* A second destination inside a table row (§6.6). One named text link in its
   own column -- never a scatter of per-row icon buttons, which is what that
   section actually forbids. */
.cell-link {
  color: var(--link);
  text-decoration: none;
  font-size: var(--fs-13);
}
.cell-link:hover { color: var(--link-hover); text-decoration: underline; }

/* The action under an empty state. _empty_state.html takes no action slot --
   deliberately, because most of this product's empty states have no route to
   offer (§5.1's note). This sits beneath it for the ones that now do. */
.empty-state__action {
  display: flex;
  justify-content: center;
  padding-block-end: var(--sp-26);
}

/* A control in a card's header, pushed to the header's end. The header is a
   flex row of title + hint; this takes the remaining space so the action sits
   opposite the title rather than jammed against the count. Added 17 Sep 2026
   for «وحدة جديدة» on Property Configuration. */
.card__action {
  margin-inline-start: auto;
}

/* One prepared WhatsApp message: the text the guest will receive, and the
   button that opens WhatsApp with it. The BODY is shown deliberately -- a deep
   link whose contents are invisible until WhatsApp opens is a message nobody
   proof-read, and these are drafts pending the product owner's wording
   (DESIGN_SYSTEM.md §9 and §6.15). */
.notify-row {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-12);
  padding: var(--sp-12);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.notify-row__body {
  margin: 0;
  flex: 1;
  min-inline-size: 0;
  color: var(--muted-warm);
  font-size: var(--fs-13);
  line-height: 1.75;
}

.notify-row .btn { flex-shrink: 0; }

@media (max-width: 640px) {
  /* The button goes full width under the text rather than squeezing the
     message into a column one word wide. */
  .notify-row { flex-direction: column; align-items: stretch; }
}

/* A one-line "nothing here" inside a card that is otherwise fine -- not §5.1's
   full empty STATE, which owns a whole screen and carries an action. */
.empty-inline {
  margin: 0;
  color: var(--muted);
  font-size: var(--fs-13);
}

/* A READ-ONLY value inside a form row -- configuration the screen shows but
   does not let you edit here. `.field label` cannot serve: there is no control
   to label, and a <label for=...> pointing at nothing is a lie to a screen
   reader. Added 16 Sep 2026 for the Property's bank account on the
   payment-record screen (DESIGN_SYSTEM.md §6.7). */
.field__label {
  color: var(--muted-2);
  font-size: var(--fs-13);
}

/* The bank account block. <pre>, because the owner typed it as lines and those
   lines are the thing staff read off to a guest -- collapsing them into a
   paragraph is what makes a digit get lost. Monospace for the same reason a
   reference number is: it is checked character by character against a bank
   statement. Always rendered dir="ltr" by its template; an IBAN is a
   left-to-right string even on an Arabic page. */
.bank-details {
  margin: 0;
  padding: var(--sp-10) var(--sp-12);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: var(--fs-13);
  line-height: 1.7;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.field .errorlist {
  padding-inline-start: 0;
  list-style: none;
  color: var(--danger-text);
  font-size: var(--fs-13);
}

/* No [type="date"] or [type="number"] here on purpose: §1.6 forbids those
   widgets outright, and a selector for a forbidden element sitting in the
   customer-facing stylesheet reads as permission. */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="tel"],
input[type="search"],
select,
textarea {
  inline-size: 100%;
  block-size: var(--control-height);
  padding-inline: var(--sp-12);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg);
  color: var(--ink);
}

textarea {
  block-size: auto;
  min-block-size: 96px;
  padding-block: var(--sp-10);
  resize: vertical;
}

/* A required field that is empty says so in words -- never a bare red
   asterisk (§6.7). Applied by the view/form, not guessed by CSS. */
.field--required-empty input,
.field--required-empty select,
.field--required-empty textarea {
  background: var(--warning-bg);
  color: var(--warning-text);
  border-color: var(--warning-border);
}
.field__required-note {
  color: var(--warning-text);
  font-size: var(--fs-12);
  font-weight: 600;
}

/* The file input. It was in none of the control selectors above, so on an
   Arabic-first RTL form it rendered as the raw browser widget: English
   "Choose File", left-aligned, about 24px tall, next to 44px tokenised rows.
   Three non-negotiables at once (§1.1 Arabic-first, §1.7's 44px, §6.7's form
   row) on the one screen whose purpose is attaching that file.
   ::file-selector-button is the only part of the control that is styleable;
   the "no file chosen" text beside it is browser chrome and stays English --
   the same category as the native date picker (§1.6), and not fixable here. */
input[type="file"] {
  inline-size: 100%;
  min-block-size: var(--control-height);
  padding: var(--sp-6) var(--sp-12);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg);
  color: var(--ink);
}
input[type="file"]::file-selector-button {
  min-block-size: 32px;
  margin-inline-end: var(--sp-12);
  padding-inline: var(--sp-14);
  border: 0;
  border-radius: var(--radius-sm);
  background: var(--chip-2);
  color: var(--ink);
  font-weight: 600;
  cursor: pointer;
}
input[type="file"]::file-selector-button:hover { background: var(--chip); }

.checkbox-row {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  /* A bare 16px checkbox was the whole hit area. §1.7 wants 44px, and on the
     sign-in screen this is the only secondary control on the page. The row
     carries the height; the box stays its native size. */
  min-block-size: var(--control-height);
}
.checkbox-row label { cursor: pointer; }
.checkbox-row input[type="checkbox"] {
  inline-size: auto;
  block-size: auto;
}

.inline-row {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
  flex-wrap: wrap;
}

fieldset {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--sp-16);
  margin: 0;
}
fieldset legend {
  padding-inline: var(--sp-8);
  font-weight: 600;
}

/* Manual Booking's TIME_SLOTS period picker (docs/PRICING_MODEL.md §2.5) --
   one date-row per booked day, each period on its own line so its
   pre-filled/editable price has room. */
.period-row {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--sp-16);
}
.period-price-input { max-inline-size: 120px; }


/* ==========================================================================
   TOGGLE (§6.8)
   Never a bare switch: always beside a title + explanation column.
   ========================================================================== */

.toggle-row {
  display: flex;
  align-items: center;
  gap: var(--sp-12);
}
.toggle-row__text {
  display: flex;
  flex-direction: column;
  line-height: 1.45;
}
.toggle-row__title { font-weight: 600; }
.toggle-row__desc { color: var(--muted); font-size: var(--fs-12); }

/* A real <input type="checkbox"> restyled, not a <button> with aria-checked:
   it submits with the form without JS, and keyboard and screen-reader
   behaviour stay the browser's own instead of being re-implemented. */
.toggle__input {
  appearance: none;
  -webkit-appearance: none;
  margin: 0;
  margin-inline-start: auto;
  flex-shrink: 0;
  inline-size: 46px;
  block-size: 26px;
  padding: 3px;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--border);
  cursor: pointer;
  display: flex;
  align-items: center;
}
.toggle__input::before {
  content: "";
  inline-size: 20px;
  block-size: 20px;
  border-radius: var(--radius-pill);
  background: var(--surface);
  transition: margin-inline-start 120ms ease;
}
.toggle__input:checked { background: var(--primary); }
.toggle__input:checked::before { margin-inline-start: 20px; }


/* ==========================================================================
   §6.20 -- THE SHEET: a core overlay vocabulary
   Added by the roles round, 15 Oct 2026, because `base.css` had none and
   three app stylesheets each had their own (`.wiz` in wizard.css,
   `.resv-modal`/`.resv-drawer` in reservations.css, `.bo-modal` in
   backoffice.css). A core screen that needed an overlay had nowhere to
   assemble one from, which is §6's "report it and pause" case -- so it is
   reported and built here, once, rather than copied a fourth time.

   NO ARTBOARD. The measurements are taken from the three that exist: the
   wizard's 38% scrim and 32px close on desktop, and `MobileWizard.dc.html`'s
   phone answer -- grab handle, 44px close, a SCROLLING body and a PINNED
   foot. It joins the designer queue (docs/HANDOFF.md section 8).

   THE PHONE HALF IS THE WHOLE REASON THE MEASUREMENTS ARE COPIED RATHER
   THAN CHOSEN. The mobile night found three overlays in this product that
   could not be reached or scrolled on a phone, and every one of them failed
   the same way: a panel taller than the viewport inside a box with
   `overflow: hidden`, with the confirm button as the last thing in the
   column. So below 640px a sheet is a bottom sheet with
   `max-block-size: 92dvh`, its body is the only scroller, and its foot is
   inside the flex column rather than `position: sticky` -- a sticky foot
   only appears once its containing block scrolls into view, which on a long
   panel is after everything else.
   ========================================================================== */

.sheet { display: contents; }

.sheet__scrim {
  position: fixed;
  inset: 0;
  z-index: 80;
  /* THE SHELL'S ONE SCRIM TECHNIQUE -- `--ink` at an opacity, never a fresh
     rgba(). The drawer, the palette, the calendar's filter sheet and the
     dashboard's modal all settled on this so the product carries ONE scrim;
     a literal rgb() would also be a hard-coded colour, which section 1.3
     forbids and which core/test_design_rules.py would NOT catch, because it
     scans for hex. .38 is the wizard's own figure, which is the overlay a
     reader of this product has already learned. */
  background: var(--ink);
  opacity: .38;
  border: 0;
  cursor: pointer;
}

/* The frame covers the CONTENT COLUMN, not the page -- the same two-box
   arrangement the wizard uses and for the same reason: one box doing both
   either dims nothing behind the sidebar or centres the panel over it. */
.sheet__frame {
  position: fixed;
  inset-inline-start: var(--sidebar-width);
  inset-inline-end: 0;
  inset-block-start: 84px;
  inset-block-end: 0;
  display: flex;
  justify-content: center;
  z-index: 90;
  pointer-events: none;
  overflow-y: auto;
}
.app--rail .sheet__frame { inset-inline-start: var(--sidebar-rail-width); }

/* THE 640-1023px BAND, AND IT IS THE BAND THIS PROJECT HAS BEEN BITTEN IN
   TWICE. At or below 1023px the `<aside>` IS the drawer -- it is off-canvas
   and the content occupies the full width -- so the two rules above would
   leave a 248px (or 72px) dead inset on the inline-start edge of every sheet
   across the whole tablet band, with no sidebar behind it. The 639px block
   further down turns the sheet into a bottom sheet and resets the inset
   there, which means the fault would have lived EXACTLY between the two
   widths anybody looks at: invisible at 390 and invisible at 1400.

   `.app--rail .sheet__frame` has to be neutralised by name and not left to
   the base rule, for the same reason `.app--rail .sidebar` is neutralised in
   the 1023px block ~500 lines below: `@media` adds no specificity, so a
   single-class rule inside a media query loses to a two-class rule outside
   it, and a reader who folded the sidebar on a laptop then opened a tablet
   would get a 72px inset instead of a 248px one -- less wrong and still
   wrong. */
@media (max-width: 1023px) {
  .sheet__frame,
  .app--rail .sheet__frame { inset-inline-start: 0; }
}

.sheet__panel {
  pointer-events: auto;
  inline-size: 560px;
  max-inline-size: 100%;
  align-self: flex-start;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  overflow: hidden;
  /* NO BOX-SHADOW. Section 1: no shadows anywhere, and the standing
     tiebreak for a popover is that section 1 wins over an artboard that
     draws one. */
}

/* A SHEET THAT IS A FORM, which is most of them — and the reason the three
   `flex` declarations below did nothing on their own.

   `.sheet__head`, `__body` and `__foot` are written to be the panel's own
   flex children. But a sheet whose contents POST wraps them in a `<form>`,
   so the panel's actual children become the handle and one block-level
   form, and every `flex` property on the three parts applies against a
   parent that is not a flex container at all. The body then takes its
   content height and the panel — which carries `overflow: hidden` for its
   radius — clips the rest.

   MEASURED on the add-employee sheet at 390x844: panel capped at 776px,
   body 1090px, «إضافة الموظف» 491px below the fold with no scroller
   anywhere. The three declarations were added first and changed NOTHING,
   which is what pointed at the form.

   An element selector rather than a class, deliberately: every sheet with a
   form wants this and none of them should have to remember a class for it.
   `> form` and not a descendant selector, so a form nested INSIDE the body
   (a sub-form in a field group) is untouched. */
.sheet__panel > form {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-block-size: 0;
}

.sheet__head {
  display: flex;
  align-items: center;
  gap: var(--sp-12);
  padding: var(--sp-14) var(--sp-18);
  border-block-end: 1px solid var(--line);
  /* `flex: none` -- the head must not be the thing that shrinks when the
     panel is capped. Without it the deficit is shared out in proportion to
     base size and the header loses its padding before the body gives up a
     pixel. */
  flex: none;
}

.sheet__text {
  flex: 1;
  min-inline-size: 0;
  display: flex;
  flex-direction: column;
}
.sheet__title {
  font-size: var(--fs-17);
  font-weight: 700;
  color: var(--ink);
}
.sheet__sub {
  font-size: var(--fs-12);
  color: var(--muted);
}

.sheet__close {
  inline-size: 32px;
  block-size: 32px;
  flex: none;
  border-radius: 9px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--muted-2);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.sheet__close:hover { border-color: var(--muted-2); }

/* THE ONLY SCROLLER. `min-block-size: 0` is what makes that true inside a
   flex column -- without it a flex item refuses to shrink below its content
   and the panel grows instead of the body scrolling, which is precisely the
   2224px-panel-in-a-780px-viewport defect the mobile night measured. */
.sheet__body {
  padding: var(--sp-18);
  overflow-y: auto;
  /* THE THREE DECLARATIONS THAT MAKE THIS THE ONLY SCROLLER, and all three
     are load-bearing -- `wizard.css`'s own phone block records the same
     three for the same reason, and this vocabulary shipped without two of
     them.

     MEASURED, on the add-employee sheet at 390x844 before the fix: the
     panel was capped at 776px by `max-block-size: 92dvh`, the body stayed
     1090px, and `.sheet__panel`'s `overflow: hidden` clipped the
     difference -- so «إضافة الموظف», the last thing in the column, was
     PAINTED AND UNREACHABLE. 491px below the fold with no scroller
     anywhere. Exactly the defect the mobile night found three times, in
     new vocabulary written after it.

     `flex: 1 1 auto` -- a flex item with `flex-basis: auto` and the
     default `flex-grow: 0` will take its content's height and let the
     container clip it. It has to be told it owns the leftover space.
     `min-block-size: 0` -- a flex item will not shrink below its content
     without it, so `overflow-y: auto` would have nothing to scroll.
     `overflow-y: auto` -- the scroller itself. */
  flex: 1 1 auto;
  min-block-size: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-16);
}

.sheet__foot {
  display: flex;
  align-items: center;
  gap: var(--sp-10);
  padding: var(--sp-14) var(--sp-18);
  border-block-start: 1px solid var(--line);
  background: var(--surface);
  /* `flex: none`, and this is the one that matters most: the foot carries
     the sheet's primary action, and an action that shrank out of its own
     padding is the second-worst outcome after one that is off screen. The
     own-ground `background` is here for the same reason it is on the
     wizard's phone header -- the foot never shows page behind it. */
  flex: none;
}
/* Pushes whatever follows it to the far end -- a destructive secondary
   action at the start, the confirm pair at the end. */
.sheet__foot-gap { margin-inline-start: auto; }

/* The grab handle: phone only, drawn by MobileWizard. Hidden on desktop
   rather than absent from the markup, so one template serves both widths. */
.sheet__handle { display: none; }

@media (max-width: 639px) {
  .sheet__frame {
    inset-inline-start: 0;
    inset-block-start: 0;
    align-items: flex-end;
    overflow: hidden;
  }
  .sheet__panel {
    inline-size: 100%;
    align-self: flex-end;
    /* `max-block-size` CAPS the panel; it does not make the panel's height
       definite, and a flex container whose height is indefinite lays its
       children out at their content size and then clips. That is the whole
       of the bug measured above. `block-size: auto` is kept — a SHORT
       sheet (the shown-once credential is 528px) must stay short rather
       than become a 92%-tall sheet holding one line — and the children's
       own `flex` declarations above are what resolve the cap into a
       scrolling body. Verified by re-measuring, not by reasoning: see
       `_review/teamsheets/probe.py`. */
    block-size: auto;
    max-block-size: 92dvh;
    border-radius: var(--radius-xl) var(--radius-xl) 0 0;
    border-block-end: 0;
  }
  .sheet__handle {
    display: block;
    inline-size: 40px;
    block-size: 4px;
    margin: var(--sp-8) auto 0;
    border-radius: var(--radius-pill);
    background: var(--border);
  }
  .sheet__close {
    inline-size: 44px;
    block-size: 44px;
  }
  .sheet__body { padding: var(--sp-16); }
  /* THE FOOT WRAPS ON A PHONE, and this is measured rather than assumed.
     The member sheet's foot carries three controls — save, reset the
     password, deactivate — and at 390px they do not fit on one row: the
     last one ran off the inline-end edge, clipped mid-word.

     IT PASSED EVERY OVERFLOW CHECK. `scripts/screenshots.py` and this
     round's own probe both measure the DOCUMENT's overflow, and the foot
     is inside a `position: fixed` panel with `overflow: hidden` — so the
     page was genuinely clean in both directions while a primary control
     was cut in half. Found by opening the picture, which is the argument
     for taking pictures at all. `_review/teamsheets/probe.py` measures the
     foot's own scrollWidth now.

     `flex: 1 1 auto` with `min-inline-size: 0` rather than a fixed basis:
     two controls share a row and a third drops to its own, and a label
     that grows in English or Arabic moves the break instead of clipping.
     `.sheet__foot-gap` keeps working — `margin-inline-start: auto` applies
     per flex LINE, so the destructive action still sits apart on whichever
     row it lands on. */
  .sheet__foot {
    padding: var(--sp-12) var(--sp-16);
    /* THE TOKEN, not inline `env()` (15 Oct 2026). This line was written
       as `env(safe-area-inset-bottom)` and was INERT the whole time, because
       the viewport meta had no `viewport-fit=cover` -- which is the defect
       documented on that meta tag. Routed through `--safe-area-bottom` so
       there is one place to check and a test can force a real inset and
       measure what moves. No fallback needed in the calc: the token carries
       its own `0px`. */
    padding-block-end: calc(var(--sp-12) + var(--safe-area-bottom));
    flex-wrap: wrap;
  }
  .sheet__foot > .btn,
  .sheet__foot > form > .btn,
  .sheet__foot > form {
    flex: 1 1 auto;
    min-inline-size: 0;
  }
}

/* ==========================================================================
   §6.19 -- THE SHOWN-ONCE CREDENTIAL, AND THE PERMISSION PANEL
   Added by the roles round, 15 Oct 2026. NO ARTBOARD EXISTS for either; both
   assemble §6's existing measurements (the 46x26 toggle above, the 32px
   action, the --surface-2 inset row) rather than inventing values, and both
   join the designer queue -- docs/HANDOFF.md section 8.

   The credential row is the owner-facing twin of `backoffice.css`'s
   `.bo-link-row` + `.bo-copy`, which show a setup token exactly once. It is a
   SECOND declaration rather than a shared one on purpose: that pair is painted
   on the bronze shell's own surfaces and lives in a stylesheet no customer
   screen loads, and the alternative -- making the team screen pull in
   backoffice.css -- would put Madar's internal vocabulary one selector away
   from a customer's page.
   ========================================================================== */

.credential {
  display: flex;
  flex-direction: column;
  gap: var(--sp-8);
}
.credential__label {
  font-size: var(--fs-13);
  font-weight: 600;
  color: var(--ink);
}

/* The inset row that holds the value and its copy control. Same
   --surface-2-on-border inset the bank-details block uses, so a credential
   reads as "a fact to take away", not as an editable field. */
.credential__row {
  display: flex;
  align-items: center;
  gap: var(--sp-10);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 11px;
  padding: var(--sp-10) 13px;
}

/* `direction: ltr` + `unicode-bidi: isolate`, for the same reason every URL
   and every reference in this product carries them: a Latin string inside an
   RTL paragraph otherwise reorders its own characters, and a password whose
   characters are displayed out of order is worse than no password at all --
   the reader types what they see and it is wrong.

   `user-select: all` so one click selects the whole value: the clipboard
   button is the primary path and this is what makes the fallback bearable. */
.credential__value {
  flex: 1;
  min-inline-size: 0;
  direction: ltr;
  unicode-bidi: isolate;
  text-align: start;
  font-size: var(--fs-17);
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--ink);
  overflow-wrap: anywhere;
  user-select: all;
}

.credential__copy {
  block-size: 32px;
  min-block-size: 32px;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-6);
  padding-inline: var(--sp-12);
  border: 0;
  border-radius: var(--radius-sm);
  background: var(--primary);
  color: var(--on-primary);
  font-family: inherit;
  font-size: var(--fs-12);
  font-weight: 700;
  cursor: pointer;
  flex-shrink: 0;
}
.credential__copy:hover { background: var(--primary-hover); }

/* The sentence under the row. It is not decoration: this value is shown
   exactly once and the reader has to be told so BEFORE they close the
   overlay, which is the only moment the warning can still be acted on. */
.credential__note {
  font-size: var(--fs-12);
  color: var(--warning-text);
  line-height: 1.5;
}

/* ---- the permission panel ------------------------------------------------
   A stack of §6.8 toggle rows with a hairline between them. The hairline and
   not a gap, because nine rows with 16px of air between them read as nine
   unrelated settings; the reader's question is "which of these does Salim
   have", which is a list. */
.perm-panel {
  display: flex;
  flex-direction: column;
}
.perm-panel .toggle-row {
  padding-block: var(--sp-12);
  border-block-start: 1px solid var(--border);
}
.perm-panel .toggle-row:first-child { border-block-start: 0; }

/* The read-only summary of what one member holds, on the members list. Pills
   in a wrapping row, so a member with every toggle wraps instead of widening
   the table -- `fr` tracks compress rather than overflow, which is how a
   table becomes one character per line on a phone. */
.perm-summary {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-6);
}
.perm-summary__none {
  font-size: var(--fs-12);
  color: var(--muted);
}

/* ==========================================================================
   THE FOUR SCREEN STATES (§5.1)
   One shape for every screen -- not designed per screen.
   ========================================================================== */

.state-panel {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-12);
  padding: var(--sp-26) var(--sp-22);
  text-align: center;
}

.state-panel__icon {
  inline-size: 56px;
  block-size: 56px;
  border-radius: var(--radius-pill);
  display: flex;
  align-items: center;
  justify-content: center;
}
.state-panel__icon svg { inline-size: 26px; block-size: 26px; }

.state-panel--empty .state-panel__icon      { background: var(--primary-soft); color: var(--success-text); }
.state-panel--error .state-panel__icon      { background: var(--danger-bg);    color: var(--danger-text); }
.state-panel--permission .state-panel__icon { background: var(--chip);         color: var(--muted-2); }

.state-panel__title {
  font-size: var(--fs-17);
  font-weight: 700;
}

.state-panel__body {
  color: var(--muted);
  max-inline-size: 380px;
}

.state-panel__actions {
  display: flex;
  align-items: center;
  gap: var(--sp-10);
  margin-block-start: var(--sp-6);
  flex-wrap: wrap;
  justify-content: center;
}

/* Loading: the page structure stays visible. No blank screen, no centred
   spinner (§5.1). */
.skeleton {
  display: flex;
  flex-direction: column;
  gap: var(--sp-10);
  padding: var(--sp-18);
}
/* The leading bar States.dc.html opens the loading panel with -- it stands in
   for the page header, which is part of "the structure stays visible". */
.skeleton__bar {
  background: var(--row);
  border-radius: var(--radius-sm);
  block-size: 34px;
  inline-size: 40%;
}
.skeleton__block {
  background: var(--row);
  border-radius: var(--radius);
  block-size: 64px;
}
.skeleton__line {
  background: var(--row);
  border-radius: var(--radius-xs);   /* 6px -- States.dc.html */
  block-size: 16px;
}
.skeleton__row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-12);
}


/* ==========================================================================
   MESSAGE COMPONENTS (§5) -- four types, icon + text, never colour alone
   ========================================================================== */

.alert {
  display: flex;
  align-items: flex-start;
  /* WRAPPING, and it is a phone fix with a desktop reason too (14 Oct 2026).
     An alert is icon + sentence + (sometimes) an action button, and the
     button does not shrink. The reservation detail's overdue-verification
     banner carries «تحقق من الدفعة الآن» — 220px in English — and at 360px
     the row ran 7px past the page edge, which is a HORIZONTAL PAGE SCROLL on
     the one screen whose banner is about something being late.

     `flex-wrap` alone is not enough: a flex item will not shrink below its
     content's minimum, so the sentence has to be allowed to. `flex: 1 1
     12rem` lets it take the room when there is room and drop the button to
     its own line when there is not. */
  flex-wrap: wrap;
  gap: var(--sp-12);
  padding: var(--sp-12) var(--sp-16);
  border-radius: var(--radius);
  border: 1px solid transparent;
  margin-block-end: var(--sp-16);
}
.alert > span,
.alert > p {
  flex: 1 1 12rem;
  min-inline-size: 0;
}

.alert svg {
  flex-shrink: 0;
  inline-size: 20px;
  block-size: 20px;
  margin-block-start: 2px;
}

.alert-success { background: var(--success-bg); border-color: var(--success-border); color: var(--success-text); }
.alert-warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning-text); }
.alert-danger  { background: var(--danger-bg);  border-color: var(--danger-border);  color: var(--danger-text); }
.alert-info    { background: var(--info-bg);    border-color: var(--info-border);    color: var(--info-text); }

.alert p { color: inherit; }
.alert ul { margin-block-start: var(--sp-4); }


/* ==========================================================================
   ACTIVITY TIMELINE (Reservation Details)
   A vertical connecting line with a dot per entry, per
   UX_PATTERNS_REFERENCE.md §5 -- bold action text, actor, timestamp, newest
   first.
   ========================================================================== */

.timeline {
  list-style: none;
  margin: 0;
  padding: 0;
  border-inline-start: 2px solid var(--border);
}
.timeline__item {
  position: relative;
  padding-inline-start: var(--sp-22);
  padding-block-end: var(--sp-22);
}
.timeline__item:last-child { padding-block-end: 0; }
.timeline__item::before {
  content: "";
  position: absolute;
  inset-inline-start: -6px;
  inset-block-start: 4px;
  inline-size: 10px;
  block-size: 10px;
  border-radius: var(--radius-pill);
  background: var(--primary);
  border: 2px solid var(--surface);
}
.timeline__meta {
  font-size: var(--fs-13);
  color: var(--muted);
}


/* ==========================================================================
   TOUCH TARGETS BELOW 1024px (§11.3 decision 2)
   01 §8.7 mandates a 44px minimum. 01 §§5-6 mandate a 42px nav item, a 38-40px
   toolbar, 32px tabs and "أزرار الترقيم 30px". Both are 01, and they
   contradict each other.
   Resolved by VIEWPORT rather than by picking a side: §§5-6's sizes are
   measurements taken from a 1440px artboard and stand at desktop width; §8.7 is
   about fingers and wins below 1024px. The same scoping already applies to
   §6.6's table row padding for the same reason.
   ========================================================================== */

@media (max-width: 1023px) {
  .tab,
  .status-chip,
  .pagination__btn,
  .calendar-view-switcher__btn,
  .site-nav__link {
    min-block-size: var(--control-height);
  }
  /* The search field is a container, so its own height is what the input
     inherits -- setting min-block-size on the wrapper is not enough.

     `align-self: stretch` AS WELL AS `block-size: 100%`, added 14 Oct 2026
     after the phone sweep measured the input at 239x22 inside a 290x44
     field. The field is `display: flex; align-items: center`, which gives
     every item `align-self: center` -- and a centred item's percentage
     height resolves against its own content box, not the line it sits on.
     So the field was 44px and the thing a thumb had to land on was the 22px
     of text in the middle of it. The parent is not a `<label>`, so a tap on
     the other 22px focused nothing. */
  .search-field {
    block-size: var(--control-height);
  }
  .search-field input {
    block-size: 100%;
    align-self: stretch;
  }
  /* A 30px square button with a 44px min-block-size would be a lozenge;
     match the inline axis so the target stays square. */
  .pagination__btn {
    min-inline-size: var(--control-height);
  }
}

/* ==========================================================================
   MOBILE (§6.10)
   The same templates at breakpoints: the sidebar becomes a drawer, a top bar
   and a bottom nav appear. Never separate pages.
   ========================================================================== */

/* ==========================================================================
   THE PHONE DRAWER'S ACCOUNT ROWS (SHELL_SPEC §3, artboard `MobileDrawer`)
   Hidden at every width where the <aside> is a sidebar; the 1023px block
   below reveals them.

   THIS RULE USED TO LIVE 1,700 LINES FURTHER DOWN, and its own comment there
   said "the 1023px block further down reveals them" — which was the whole
   defect, because that block is ABOVE it. `@media` adds no specificity and
   both rules are (0,1,0), so the later one won and **the drawer's account
   block never rendered at any width.**

   WHAT WAS UNREACHABLE ON A PHONE, for as long as it stood: «اللغة»,
   «المظهر», the account-and-subscription row, and «تسجيل الخروج». There was
   no way to sign out of this product on a phone. It compounds the shell
   defect the same round fixed — the account control is deliberately not in
   the phone bar BECAUSE `MobileDrawer` carries it, and the drawer was not
   carrying it.

   Found by driving the drawer open at 390px and 768px in
   `_review/mobile-night/overlays.py`, which reported the panel HIDDEN on a
   shell that had just been rebuilt to put it there. Pre-existing: the same
   two rules sit in the same order in `f2129b5`.
   ========================================================================== */

.drawer-account { display: none; }


/* Mobile-only chrome, hidden by default and shown below 1024px.
   .topbar-band is here for the same reason the bottom nav is: without a
   default rule it inherits nothing and would render its phone-only contents
   (the short-label switcher) on desktop, beside the desktop switcher it
   replaces.

   `.topbar` IS NOT IN THIS LIST because there is no longer such an element
   (the two bars merged, 14 Oct 2026). Its screen-supplied parts --
   `.topbar__text`, `.topbar__actions`, `.topbar__btn` -- live inside
   `.appbar` now and are hidden by that bar's own desktop rules. */
.topbar-band,
.bottom-nav,
.drawer-scrim,
/* `.drawer-close` JOINS THIS LIST, 15 Oct 2026 (device-matrix round), and it
   was a live defect rather than tidying. Its ONLY rule lived inside the
   `@media (max-width: 1023px)` block, so above that width nothing overrode
   the UA default for a `<button>` and it simply rendered: measured at
   1920x1080 as `display: block`, visible, 220x19px at (1686, 76) -- on
   screen, clickable, and sitting on a desktop where the sidebar is not a
   drawer and there is nothing to close. Reproduced at 1194px too, so it is a
   width-threshold bug and not a desktop one. Below 1023px the phone block
   still sets `display: flex` at 44x44, so hiding it here costs the drawer
   nothing.

   It also closed a tap-target finding for free: that unstyled 19px-tall box
   was one of the controls the matrix reported under 40px, and the honest fix
   was never to enlarge it -- it was to stop painting it. */
.drawer-close { display: none; }

/* THE TWO-BAR SHELL AND WHY IT IS GONE (14 Oct 2026).

   This comment used to explain how `.appbar` (desktop) and `.topbar` (phone)
   were kept "exactly complementary at every width: above 1023 the app bar, at
   or below it the phone bar, and never both". That arrangement is replaced by
   ONE bar whose appearance changes at 1023px -- see `.appbar`'s own block
   below -- because complementary is a property two rules have to maintain and
   one element simply has.

   WHAT IT COST WHILE IT STOOD, kept because both halves are instructive. The
   hiding rule was once written 834 lines above `.appbar`'s definition, lost
   the cascade at equal specificity, and the desktop bar rendered on every
   phone with its end cluster 161px off the page at 375px. And the phone bar
   it left standing carried ☰ and the wordmark and nothing else, so the bell,
   the notifications panel and global search had no phone affordance at all --
   the product owner's shell-parity finding, and the reason for the merge.

   THE 639px SCOPING SURVIVES UNCHANGED, and it is the half that still
   matters. A screen's own title and action tiles appear only BELOW 640px, so
   across 640-1023px the bar is ☰ + wordmark + the shell's two tiles and the
   screen keeps its own page header. Anchoring that at 1023px instead is the
   mistake that broke every calendar screen across that band once, and it is
   why 768px is a permanent probe width. */

@media (max-width: 1023px) {

  /* `.appbar { display: none; }` WAS HERE and has moved to sit immediately
     after `.appbar`'s own definition, 834 lines below. It lost the cascade
     from here -- same specificity, and the definition came later -- so the
     desktop bar rendered at every width. See the note at the new location. */

  /* The collapse handle goes with it: at this width the <aside> IS the
     drawer, and there is no rail to fold it to. The `.app--rail` class can
     still be on the shell from a desktop session, so the rail's own
     declarations are neutralised here too -- otherwise a reader who folded
     the sidebar on a laptop would open the phone and find a 72px drawer
     with no labels. */
  .sidebar__collapse { display: none; }
  .app--rail .sidebar {
    inline-size: var(--sidebar-width);
    padding-inline: var(--sp-14);
  }
  /* `.app--rail .wordmark__text` is NOT in this list any more: the rule it
     reverted is gone, because the only wordmark inside `.app` is the top
     bar's and the rail must not touch it (15 Oct 2026). */
  .app--rail .site-nav__label,
  .app--rail .site-nav__chev,
  .app--rail .site-nav__children { display: revert; }
  .app--rail .site-nav__link,
  .app--rail .site-nav__child {
    justify-content: flex-start;
    padding-inline: var(--sp-12);
  }
  .app--rail .site-nav__badge {
    position: static;
    margin-inline-start: auto;
  }
  /* THE DRAWER'S ACCOUNT ROWS appear (SHELL_SPEC §3, `MobileDrawer`). The
     desktop foot has none of this; the drawer carries the whole account
     block, because a phone has no top bar to put it in. */
  .drawer-account {
    display: flex;
    flex-direction: column;
    border-block-start: 1px solid var(--sidebar-line);
    margin-block-start: var(--sp-10);
    padding-block-start: var(--sp-6);
  }
  .drawer-account__row {
    display: flex;
    align-items: center;
    gap: var(--sp-10);
    /* 44px -- README rule 6, and these are the drawer's own value rows. */
    min-block-size: var(--nav-item-height);
    padding-inline: var(--sp-12);
    margin: 0;
  }
  .drawer-account__label {
    flex: 1;
    min-inline-size: 0;
    font-size: var(--fs-14);
    color: var(--on-dark);
  }
  .drawer-account__value { font-size: var(--fs-13); color: var(--sidebar-text); }
  .drawer-account__soon {
    font-size: var(--fs-11);
    font-weight: 600;
    color: var(--sidebar-dim);
  }
  /* The drawer's «English» / «العربية» switch (i18n round). The same
     manners as `.drawer-account__cycle` beside it -- a bare button whose
     tap target is the whole row's height, not its text's -- but it is a
     real submit in a form rather than a cycling button, because changing
     language changes the document's direction and there is nothing to
     swap in place. `--sidebar-accent` rather than `--sidebar-text`: it is
     the row's one ACTION, and the value beside it is already the quieter
     colour.

     Measured on the sidebar ground, which is a dark surface in BOTH
     themes: 9.07:1 light (#7FD69B on #14261A) and 11.99:1 dark (on
     #000000). `--sidebar-accent` is declared once, in the light block,
     so only the ground beneath it changes between themes.

     (The first draft of this comment claimed 5.21:1 and 6.30:1. Both
     were invented rather than measured, and the measurement is two
     lines of arithmetic -- which is the whole reason this project
     states figures instead of asserting compliance.) */
  .drawer-account__switch {
    background: none;
    border: 0;
    color: var(--sidebar-accent);
    font-family: inherit;
    font-size: var(--fs-13);
    font-weight: 600;
    cursor: pointer;
    min-block-size: var(--nav-item-height);
    padding: 0;
    margin-inline-start: auto;
  }

  .drawer-account__cycle {
    display: flex;
    align-items: center;
    gap: var(--sp-6);
    background: none;
    color: var(--sidebar-text);
    cursor: pointer;
    /* The tap target is the whole row's height, not the text's. */
    min-block-size: var(--nav-item-height);
    padding: 0;
  }
  .drawer-account__user {
    display: flex;
    align-items: center;
    gap: 11px;
    min-block-size: var(--nav-item-height);
    padding: var(--sp-10) var(--sp-12);
    border-block-start: 1px solid var(--sidebar-line);
    text-decoration: none;
    color: inherit;
  }
  .drawer-account__user .icon { color: var(--sidebar-dim); margin-inline-start: auto; }
  .drawer-account__out {
    display: flex;
    align-items: center;
    gap: var(--sp-10);
    inline-size: 100%;
    min-block-size: var(--nav-item-height);
    padding-inline: var(--sp-12);
    border-block-start: 1px solid var(--sidebar-line);
    background: none;
    cursor: pointer;
    text-align: start;
    font-size: var(--fs-14);
    font-weight: 600;
    /* On the sidebar's dark ground, --danger-text (#8A372C) is unreadable;
       --danger-on-dark is the package's own value for this (theme.py maps the
       artboard's logout red to it). Measured: worst 5.82:1. */
    color: var(--danger-on-dark);
  }
  .drawer-account__out .icon { color: var(--danger-on-dark); }

  /* A fixed, viewport-sized clip box around the drawer.
     WITHOUT THIS the closed drawer extends the document: it is parked at
     inset-inline-start: -248px, and in RTL the inline-start side is the RIGHT,
     so "off-screen" means off the right edge -- which browsers DO count in
     scrollWidth (unlike off the left edge in LTR). Measured directly: 390px of
     scrollWidth in a 375px viewport on every mobile page. An LTR-only test
     would never have caught it.
     pointer-events: none so this invisible full-viewport box doesn't swallow
     taps meant for the page or the scrim beneath it. */
  .drawer-shell {
    display: block;
    position: fixed;
    inset-block: 0;
    inset-inline: 0;   /* not `inset: 0` -- that is the physical shorthand, and
                          it is the one form that slips past both §1.2 and the
                          test that enforces it */
    z-index: 40;
    overflow: hidden;
    pointer-events: none;
  }

  .drawer-close {
    display: flex;
    position: absolute;
    inset-block-start: var(--sp-16);
    /* inset-inline-END. MobileDrawer.dc.html puts the close at the corner
       FARTHEST from the wordmark -- the panel's left in RTL. `start` put it
       straight on top of the «مُ» glyph, which the capture showed at once. */
    inset-inline-end: var(--sp-12);
    inline-size: var(--control-height);
    block-size: var(--control-height);
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 12px;
    background: var(--sidebar-active);
    color: var(--sidebar-text);
    cursor: pointer;
    z-index: 1;
  }

  .sidebar {
    position: absolute;   /* .drawer-shell is fixed, so it is the containing
                             block -- and therefore clips this */
    inset-block: 0;
    /* MobileDrawer.dc.html draws the panel at 296px, wider than the desktop
       sidebar's 248px, with padding 16px 12px 14px. Blueprint «Phone» keeps the
       CONTENTS identical ("Same items, same badges, same order") -- it is
       the panel that differs, not the menu.
       The artboard also draws a box-shadow on the panel
       (-18px 0 44px rgba(0,0,0,.34)). NOT BUILT, and not a judgement call:
       docs/DESIGN_SYSTEM.md bans shadows product-wide, and this round's own
       precedent is recorded -- wave 2's filter drawer omitted both its
       artboards' shadows for the same reason. Flagged for the designer in
       HANDOFF §8 rather than resolved here. */
    inline-size: 296px;
    /* 68px AT THE TOP, NOT THE DRAWN 16 (15 Oct 2026). `.drawer-close` is
       absolute at `inset-block-start: 16px` and is 44px tall, so it owns
       0-60px of the panel's inline-end corner. Until this round the
       wordmark sat under it and gave the nav its clearance; the wordmark is
       in the top bar now, and at 16px the first nav row rendered straight
       under the × with its label behind the button. 16 + 44 + 8 = 68 keeps
       the rhythm the wordmark used to produce (34px glyph + its own 22px
       bottom padding + 16 = 72). */
    padding: 68px 12px 14px;
    inset-inline-start: -296px;
    pointer-events: auto;
    /* THE DRAWER SCROLLS ITS OWN CONTENTS. It holds seven nav rows, the
       settings row, the language and appearance rows, the user block and
       «تسجيل الخروج» -- which fits a 780px phone with room to spare and does
       NOT fit a 667px one (an iPhone SE, which is still the shortest phone an
       owner is likely to be holding). Without this the overflow goes to the
       page behind the scrim, where nothing can reach it: `inset-block: 0`
       pins the panel to the viewport. `overscroll-behavior` stops the page
       scrolling once the drawer hits its end. */
    overflow-y: auto;
    overscroll-behavior: contain;
    /* visibility, not just position: an off-screen drawer that is merely
       moved is still in the tab order, so a keyboard user on a phone tabs
       into an invisible menu. visibility: hidden removes it from the tab
       order; the 180ms delay holds it visible until the slide finishes. */
    visibility: hidden;
    /* inset-inline-start, not transform: translateX() is physical and would
       slide the drawer off the wrong edge under dir="ltr" (§1.2). */
    transition: inset-inline-start 180ms ease, visibility 0s linear 180ms;
  }
  .sidebar[data-open="true"] {
    inset-inline-start: 0;
    visibility: visible;
    transition: inset-inline-start 180ms ease;
  }

  /* `.sidebar__date { display: none }` WAS HERE and is gone with the line
     itself (15 Oct 2026). It existed because the aside IS the drawer at
     this width and MobileDrawer.dc.html draws no date; the sidebar has no
     header at all now, so there is nothing to hide. The date is a row in
     the account menu, which the drawer's own user block reaches. */

  .drawer-scrim {
    display: block;
    position: fixed;
    inset-block: 0;
    inset-inline: 0;
    z-index: 30;
    /* The shell's own scrim technique at the artboard's opacity -- the same
       ruling wave 2's drawer took, so the product carries one scrim rather
       than a third ad hoc rgba(). MobileDrawer draws rgba(12,18,14,.46). */
    background: var(--ink);
    opacity: 0.46;
    border: 0;
  }

  /* THE PHONE BAR'S OWN RULES ARE NOT HERE. They are beside `.appbar`'s
     definition, ~700 lines below, because that is the only place they can
     win: `@media` adds no specificity, so a rule written here loses to the
     base `.appbar__search` / `.appbar__end` / `.appbar__icon-btn` rules
     declared after it. Keeping both halves of one bar in one place is also
     the point of having merged them.

     What WAS here -- `.topbar`, `.topbar .wordmark`, `.topbar__btn`,
     `.topbar__btn--end` -- moved with it. */

  /* `position: fixed`, NOT `sticky` -- the owner's iPhone defect, and the
     reason is specific enough to be worth the paragraph (15 Oct 2026).

     A `sticky` bottom bar sticks to the bottom of its SCROLLPORT, and for a
     root-scrolling page the scrollport's height is the LAYOUT viewport. On
     iOS Safari the layout viewport is the large, toolbar-hidden height --
     it does not shrink when the toolbar appears. So on every page long
     enough to scroll, a sticky bottom bar renders at the bottom of a
     viewport that extends underneath Safari's own toolbar, which is exactly
     what the owner saw: the nav, the toggle list and the plus button
     covered or off-screen.

     Switching `.app` to `dvh` does NOT fix this on its own, and believing
     it did would have shipped a half-fix. `dvh` makes a SHORT page's nav
     land correctly, because there is no scrolling and the bar sits at its
     static position. Every long screen in this product -- the Dashboard
     measured 2621px tall at 430x932, Guests 5070px -- still scrolls, and a
     sticky bar in a scrolling page is still pinned to the layout viewport.
     `fixed` is positioned against the VISUAL viewport on iOS Safari, which
     is the one the reader can see, so it sits flush above the toolbar.

     z-index 20 is unchanged and still correct: the drawer scrim is 30, the
     sheets 80/90, so going out of flow cannot put this bar over an overlay. */
  .bottom-nav {
    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    align-items: center;
    position: fixed;
    inset-inline: 0;
    inset-block-end: 0;
    z-index: 20;
    background: var(--surface);
    border-block-start: 1px solid var(--border);
    /* §6.9's 20px gesture area PLUS the device's own inset, and the sum is
       deliberate rather than `max()` of the two. The artboard's 20px is the
       breathing room under the labels; the inset is the strip the home
       indicator physically occupies. They are not the same allowance, so a
       phone with an indicator owes both: 20px at rest, 54px on a notched
       iPhone. `max()` would have put the labels right on the indicator's
       top edge. Flagged to the owner for his real-device pass, because
       which of the two reads better is a thing only an eye on the phone can
       settle -- docs/HANDOFF.md §8. */
    padding-block: var(--sp-8) calc(var(--sp-20) + var(--safe-area-bottom));
    /* THE INLINE INSETS AGAIN, AND THIS BAR HAS TO PAY THEM ITSELF. `.app`
       pads its own column, but a `fixed` element is positioned against the
       INITIAL containing block, not against `.app` -- so going out of flow
       also left `.app`'s protection behind. At 932px wide a landscape
       iPhone is still under the 1023px phone breakpoint, so this bar does
       render there, with ~47px of each end under the notch. */
    padding-inline: calc(var(--sp-12) + var(--safe-area-start))
                    calc(var(--sp-12) + var(--safe-area-end));
  }
  /* «المزيد» is a <button> (it opens the drawer, it is not a destination --
     S6), so this rule now also has to undo the UA button chrome the four
     <a>/<span> items never carried. `flex: 1` was on the items implicitly via
     the nav's own layout; it is stated here because a button does not stretch
     the way an anchor did. */
  .bottom-nav__item {
    display: flex;
    flex: 1;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    min-block-size: var(--control-height);
    justify-content: center;
    border: 0;
    padding: 0;
    background: transparent;
    font: inherit;
    color: var(--muted);
    text-decoration: none;
    font-size: var(--fs-11);
    cursor: pointer;
  }
  /* Weight as well as colour: §1.10 forbids colour being the only signal,
     and MobileDashboard.dc.html renders the active label at 700. */
  .bottom-nav__item--active { color: var(--link); font-weight: 700; }
  .bottom-nav__item svg { inline-size: 21px; block-size: 21px; }

  .bottom-nav__cell { display: flex; justify-content: center; }
  .bottom-nav__action {
    inline-size: 54px;
    block-size: 54px;
    border-radius: var(--radius-xl);
    background: var(--primary);
    color: var(--on-primary);
    display: flex;
    align-items: center;
    justify-content: center;
    text-decoration: none;
  }
  .bottom-nav__action svg { inline-size: 24px; block-size: 24px; }

  /* THE RESERVE THE FIXED BAR OWES BACK. Taking `.bottom-nav` out of flow
     bought a bar that sits above Safari's toolbar and cost the space it used
     to occupy in the column -- so without this, the last row of every long
     screen sits underneath it. That is the defect a `fixed` bottom bar
     trades for, and it is the one half of this change that is fully
     measurable off-device, so it is gated: `core/test_device_gates.py`
     measures each bar's real height and fails if the reserve is not equal
     to it.

     On `.app__main` rather than on `body`, because `.app__main` is the
     column the content actually lives in -- padding the body would also
     push the sidebar and the drawer.

     TWO BARS, NEVER BOTH: `.app--form` hides `.bottom-nav` and shows
     `.form-action-bar` instead (~400 lines below), so the form shell
     reserves that bar's height and not this one's.

     `:has(> .bottom-nav)` AND NOT A BARE `.app__main`, and this is a defect
     the round introduced and caught rather than a refinement. The nav is
     wrapped in `{% if not shell_locked %}` (templates/base.html), so the
     LOCKED shell -- the whole seven-step onboarding flow -- renders no nav
     at all. A bare reserve gave every onboarding screen on a phone 83px of
     empty space below its last card, which is precisely the "floating with
     dead space below" the owner objected to, reintroduced one screen over.

     The locked shell carries no CSS class to target (`shell_locked` is
     template-only logic), so the alternative was inventing `app--locked`
     and remembering to apply it to every future shell that drops the nav.
     Keying off the nav's actual PRESENCE cannot fall out of step with it.
     This is the first `:has()` in the product; it is well past baseline in
     every engine this ships to.

     The nav is a child of `.app__main`, after `</main>` -- which is why the
     combinator is `>` and why padding this element is what opens the gap. */
  .app__main:has(> .bottom-nav) {
    padding-block-end: calc(var(--bottom-nav-height) + var(--safe-area-bottom));
  }
  /* `.app.app--form` for 0,3,0 rather than relying on source order: the rule
     above resolves to 0,2,0 (`:has()` takes its argument's specificity), and
     a tie decided by document order is a tie somebody loses by reordering
     the file. A form shell hides the nav but keeps it in the DOM, so the
     selector above still matches and this has to beat it. */
  .app.app--form .app__main {
    padding-block-end: calc(var(--form-action-bar-height) + var(--safe-area-bottom));
  }

  .page-header { flex-direction: column; align-items: stretch; }
  .page-header__actions { margin-inline-start: 0; }
  .search-field { inline-size: 100%; }
}


/* ==========================================================================
   PHASE B ADDITIONS (15 Sep 2026)
   Four class groups four Phase B screens need and Phase A did not have.
   Authored by the build agent that hit the gap, which may not edit this file:
   it reported the need and supplied the CSS rather than adding a <style> block
   or a local copy. That is the process working -- a local workaround here is
   exactly what would have fragmented the vocabulary again.

   Vetted before adoption: zero hard-coded hex, zero physical left/right, and
   all 33 tokens referenced are defined. `--split` is a per-screen custom
   property supplied by the template, like `--cols` -- both are whitelisted in
   core/test_design_rules.py.
   ========================================================================== */
/* ==========================================================================
   PROPORTIONAL PAGE SPLIT (§6)
   The one class every two-or-more-column screen in Phase B needs. A screen
   supplies its track list through --split, exactly as .data-table--grid
   already takes --cols:

     <div class="split" style="--split: 300px 1fr">      Property Config
     <div class="split" style="--split: 268px 1fr">      Unit Config
     <div class="split" style="--split: 1.4fr 1fr 1fr 1fr">  a period card

   B1 and B2 need the same shape (ReservationDetail 1.6fr 1fr, NewBooking
   1fr 360px, PaymentVerify 1.1fr 1fr) -- one class, six screens, no bespoke
   grid per screen (§0's assemble-don't-invent rule).

   Collapses to one column below 1024px, which is what every one of those
   layouts must do on a phone anyway.
   ========================================================================== */

.split {
  display: grid;
  grid-template-columns: var(--split, minmax(0, 1fr));
  gap: var(--sp-16);
  align-items: start;
}

/* `1fr` is `minmax(auto, 1fr)`, and `auto` floors the track at the item's
   MIN-CONTENT width -- so one wide child (a table, a long unbroken string)
   silently widens the whole grid instead of scrolling inside itself. Measured
   on Property Configuration at 375px: 385px of document in a 375px viewport,
   overflowing off the LEFT, which is the direction RTL overflows.
   Same trap as .app__main's min-inline-size: 0. */
.split > * { min-inline-size: 0; }

@media (max-width: 1023px) {
  .split { grid-template-columns: minmax(0, 1fr); }
}


/* ==========================================================================
   SECTION NAV (PropertyConfig.dc.html, UnitConfig.dc.html)
   The vertical section list beside a configuration form. 44px rows -- the
   artboard's own measurement AND §1.7's touch minimum agree here.
   ========================================================================== */

.section-nav {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.section-nav__item {
  display: flex;
  align-items: center;
  gap: var(--sp-10);
  min-block-size: var(--control-height);
  padding-inline: var(--sp-12);
  border-radius: var(--radius-sm);
  color: var(--muted-2);
  text-decoration: none;
}

.section-nav__item:hover {
  background: var(--chip);
  color: var(--ink);
}

/* The completeness marker sits at the far end of the row. The screens pass a
   real status pill (dot AND word) rather than the artboard's bare coloured
   dot -- §1.10 forbids colour being the only signal. */
.section-nav__item > :last-child { margin-inline-start: auto; }


/* ==========================================================================
   CHOICE CARDS (UnitConfig.dc.html)
   A radio group drawn as cards, so each option's consequence is visible
   before choosing. The <input type="radio"> stays real, visible and
   focusable -- this is decoration on a working control, never a replacement
   for one.
   ========================================================================== */

/* The column count is a per-screen custom property, like --cols and --split
   (§6.12). It was hardcoded to 2, which is right for Unit Configuration's
   booking-mode pair and wrong for New Booking, whose artboard puts Unit cards
   and Period cards in threes -- a third card wrapped to its own row. The build
   agent reported that rather than adding a local override, which is why it is
   fixed once here instead of twice differently.
   Defaults to 2 so the existing caller needs no change. */
.choice-cards {
  display: grid;
  grid-template-columns: var(--choice-cols, repeat(2, minmax(0, 1fr)));
  gap: var(--sp-12);
  margin-block-end: var(--sp-16);
}

@media (max-width: 640px) {
  .choice-cards { grid-template-columns: 1fr; }
}

.choice-card {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  padding: var(--sp-14);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
}

.choice-card:hover { background: var(--bg); }

/* The artboard uses a 2px border on the selected card. Declared on both
   states so selecting one does not shift the row by a pixel. */
.choice-card { border-width: 2px; border-color: var(--border); }
.choice-card--selected {
  border-color: var(--primary);
  background: var(--primary-soft);
}

.choice-card__head {
  display: flex;
  align-items: center;
  gap: var(--sp-8);
}

.choice-card__title { font-weight: 700; }
.choice-card__desc { color: var(--muted); font-size: var(--fs-13); }

.choice-card input[type="radio"] {
  inline-size: auto;
  block-size: auto;
  margin: 0;
  flex-shrink: 0;
  accent-color: var(--primary);
}


/* ==========================================================================
   SIGN-IN (Login.dc.html) -- the one pre-authentication screen
   Two halves, 1fr 1fr: a dark brand side and a 420px form column.
   The brand side reuses the --sidebar scale, which is a dark surface in BOTH
   themes -- that is exactly what this panel is, so no new token is needed and
   no pairing goes unmeasured.
   ========================================================================== */

.auth {
  display: grid;
  grid-template-columns: 1fr 1fr;
  /* dvh with a vh fallback -- see `.app`'s own block for why. This screen
     REPLACES the shell (it is the one page that does not extend it), so it
     does not inherit `.app`'s sizing and needs its own. */
  min-block-size: 100vh;
  min-block-size: 100dvh;
}

.auth__brand {
  background: var(--sidebar);
  color: var(--on-dark);
  padding: var(--sp-48) var(--sp-64);
  display: flex;
  flex-direction: column;
}

.auth__pitch {
  margin-block: auto;
  display: flex;
  flex-direction: column;
  gap: var(--sp-22);
}

/* 38px, not the artboard's 40px: 38 is on §3's scale and 40 is not. */
.auth__headline {
  margin: 0;
  font-size: var(--fs-38);
  font-weight: 700;
  line-height: 1.35;
  letter-spacing: -1px;
}

.auth__lede {
  margin: 0;
  color: var(--sidebar-text);
  font-size: var(--fs-15);
  max-inline-size: 420px;
}

.auth__points {
  display: flex;
  flex-direction: column;
  gap: var(--sp-14);
  padding-block-start: var(--sp-10);
}

.auth__point {
  display: flex;
  align-items: center;
  gap: 11px;
}

.auth__point-mark {
  inline-size: 26px;
  block-size: 26px;
  flex-shrink: 0;
  border-radius: var(--radius-pill);
  background: var(--sidebar-active);
  color: var(--on-dark);
  display: flex;
  align-items: center;
  justify-content: center;
}

.auth__foot {
  color: var(--sidebar-dim-2);
  font-size: var(--fs-12);
}

.auth__panel {
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-48) var(--sp-26);
}

.auth__column {
  inline-size: 100%;
  max-inline-size: 420px;
  display: flex;
  flex-direction: column;
  gap: var(--sp-22);
}

.auth__heading {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}

/* The artboard's sign-in button spans the form column. */
.auth__submit { inline-size: 100%; }

/* The .card__body inside .auth__column carries the field stack; its last
   field must not leave a dangling margin above the button. */
.auth__column .field:last-of-type { margin-block-end: 0; }

@media (max-width: 1023px) {
  .auth { grid-template-columns: 1fr; min-block-size: 0; }
  .auth__brand { padding: var(--sp-26) var(--sp-22); gap: var(--sp-22); }
  .auth__pitch { margin-block: 0; gap: var(--sp-16); }
  .auth__headline { font-size: var(--fs-26); }
  .auth__panel { padding: var(--sp-26) var(--sp-16); }
}

/* The elision in a paginated footer. Not a button: it is not a destination,
   and a focusable "..." in a row of page numbers is a keyboard trap. Sized to
   match .pagination__btn's line so the row stays on one baseline. */
.pagination__gap {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: var(--sp-20);
  color: var(--muted);
  font-size: 13px;
}

/* Below 640px the numbered pages collapse to just the current one.
   MEASURED, not assumed: a full seven-page window at 375px wrapped to SIX rows
   and 181px -- «السابق» + 1 + … + 5 6 7 8 9 10 11 + … + 16 + «التالي» is
   fourteen controls in a 341px row. .pagination wraps rather than scrolls, so
   that does not overflow; it just turns the footer into a sixth of the phone
   screen, which no overflow check reports. Reasoning said seven would fit. It
   does not.

   Prev/next are the controls a thumb actually uses on a phone; the numbers are
   a desktop affordance. The current page stays so the user still knows where
   they are, and the range line above it («176–200 من 400») carries the total. */
@media (max-width: 639px) {
  .pagination__btn--page:not(.pagination__btn--active),
  .pagination__gap {
    display: none;
  }
}

/* THE DIRECTIONAL ICONS (§6.11, and I18N_SPEC's second move).
   `back`, `chevron` and `collapse` are authored pointing along the LTR
   inline axis and mirrored for RTL, so one glyph and its mirror can never
   drift apart the way two hand-drawn paths would.

   AN INHERITED MULTIPLIER RATHER THAN A DIRECT TRANSFORM, and the reason is
   a real collision: `.tl-nav__btn--flip svg` sets `transform: scaleX(-1)`
   on the very same <svg> to make the period-PREVIOUS button. Two transforms
   on one element do not compose -- the more specific wins -- and
   `[dir="rtl"] .icon--directional` (0,2,0) beats `.tl-nav__btn--flip svg`
   (0,1,1), so "previous" would have silently stopped flipping in Arabic the
   moment chevron became directional.

   A custom property inherits, so direction sets it once on <html> and any
   control that means "the other way" overrides it for its own subtree. LTR
   needs no rule at all: the default is 1, which is how the glyph is drawn. */
.icon--directional {
  transform: scaleX(var(--icon-dir, 1));
}

[dir="rtl"] {
  --icon-dir: -1;
}

/* A control whose job is the REVERSE direction. In LTR it flips the glyph;
   in RTL the glyph is already flipped, so flipping again returns it -- which
   is why the second rule is not redundant. */
.tl-nav__btn--flip {
  --icon-dir: -1;
}

[dir="rtl"] .tl-nav__btn--flip {
  --icon-dir: 1;
}

/* ============================================================
   Form action bar (§6.10, §11.3 item 4)
   ============================================================
   ONE element, ONE primary button, two appearances. Not a second button that
   appears on mobile: §1.8 allows one primary per screen, and two in the DOM --
   even with only one ever visible -- is the shape that makes "which button did
   the user actually tap" unanswerable from the markup. core/test_design_rules
   and a payments view test both count them, and both were right to.

   Desktop: an ordinary in-flow row. There is no bottom nav to collide with at
   that width, and a sticky bar on a 1400px screen is chrome solving nothing.

   Below 1024px: sticky to the bottom, and the global bottom nav is hidden
   (.app--form). Sticky rather than fixed, so it occupies its own space and can
   never cover the last field -- the failure that needs a device to notice. */
.form-action-bar {
  display: flex;
  align-items: center;
  gap: var(--sp-12);
  margin-block-start: var(--sp-16);
}

/* The running total, where a screen has one. Hidden on desktop, where the
   screen's own summary is still on-screen. */
.form-action-bar__total {
  display: none;
}

.form-action-bar .btn-primary {
  margin-inline-start: auto;
}

@media (max-width: 1023px) {
  .app--form .bottom-nav {
    display: none;
  }

  /* `fixed` and safe-area padded for the same reasons as `.bottom-nav` above
     -- read that block's comment; this is the bar that replaces it on a form
     shell, so the two cannot be allowed to drift apart. It is the bar
     carrying «التالي» and the running total on the booking wizard, which
     makes it the bottom element a guest-facing flow depends on most. */
  .form-action-bar {
    position: fixed;
    inset-inline: 0;
    inset-block-end: 0;
    z-index: 20;
    justify-content: space-between;
    min-block-size: 52px;
    /* The gesture area, the same allowance .bottom-nav makes (§6.9), plus
       the device inset. */
    padding-block: var(--sp-8) calc(var(--sp-20) + var(--safe-area-bottom));
    /* Out of flow, so it pays the inline insets itself -- see `.bottom-nav`. */
    padding-inline: calc(var(--sp-12) + var(--safe-area-start))
                    calc(var(--sp-12) + var(--safe-area-end));
    background: var(--surface);
    border-block-start: 1px solid var(--border);
  }

  .form-action-bar__total {
    display: flex;
    flex-direction: column;
    font-size: 12px;
    color: var(--muted-2);
  }

  .form-action-bar__amount {
    font-size: 17px;
    font-weight: 700;
    color: var(--ink);
  }
}


/* ==========================================================================
   THE PHONE APP BAR — BELOW 640px ONLY.

   The blueprint's «Phone» section scopes the collapse to "Below 640px the
   app bar and the page header collapse into a single bar". These rules
   lived in the shell's 1023px block for one round, which put the phone bar
   over the DESKTOP grid at 640-1023px with no page header, no day strip and
   no controls -- a state no artboard and no section describes. Found by
   review; PROBE_WIDTHS had no width in that band, so nothing automated
   could see it, and 768 is now probed forever.

   Above 639px this whole band is inert: .topbar__text, .topbar__actions and
   .topbar-band are display:none, the wordmark shows, and a calendar screen
   is back to its page header and desktop controls exactly as before the
   phone bar existed.
   ========================================================================== */

.topbar__text,
.topbar__actions { display: none; }

@media (max-width: 639px) {
  /* The title slot: 17px/700 over an 11px sub-line that ellipsises rather
     than wrapping the bar to two lines (MobileShell.dc.html). */
  .topbar__text {
    display: block;
    flex: 1;
    min-inline-size: 0;
    line-height: 1.3;
  }
  /* BOTH ARE display:block, and that is the whole of S5. They are <span>s
     inside a `display: block` container, so without this they flowed onto ONE
     line -- the app bar rendered «التقويم شاليهات الباطنة · 4 وحدات» where
     every phone artboard stacks a 17px/700 title over an 11px sub-line. The
     container being block was not enough; the children are inline by default. */
  .topbar__title {
    display: block;
    color: var(--on-dark);
    font-size: 17px;
    font-weight: 700;
    line-height: 1.3;
  }
  .topbar__sub {
    display: block;
    color: var(--sidebar-text);
    font-size: 11px;
    line-height: 1.3;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* The screen's two surviving icon buttons. */
  .topbar__actions {
    display: flex;
    align-items: center;
    gap: 9px;
  }

  /* A screen that supplies a title replaces the wordmark with it -- one
     bar, one identity. A screen that supplies none keeps the wordmark at
     every width. */
  /* A screen that supplies a title replaces the wordmark with it -- one
     bar, one identity. THE ANCESTOR USED TO BE `.topbar` AND THAT ELEMENT IS
     GONE (the bars merged, 14 Oct 2026), so for one capture every phone-bar
     screen drew its title AND the wordmark, overlapping: «التقوي…» and
     «مُدار» in the same 390px row, with the title ellipsised to nothing. */
  .app--phone-bar .appbar .wordmark { display: none; }

  /* The band under the bar, same dark ground (MobileShell: 0 12px 12px). */
  .topbar-band {
    display: block;
    background: var(--sidebar);
    padding: 0 12px 12px;
  }

  /* The segmented view switcher inside that band: container
     --sidebar-active at radius 12 with 2px padding and gaps, four EQUAL
     44px tabs at radius 10, no scroll. Active tab is the same inverted
     pair the day strip's today-chip uses. */
  .phone-tabs {
    display: flex;
    gap: 2px;
    padding: 2px;
    border-radius: 12px;
    background: var(--sidebar-active);
  }
  .phone-tab {
    flex: 1;
    block-size: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 10px;
    font-size: var(--fs-12);
    font-weight: 400;
    color: var(--sidebar-text);
    text-decoration: none;
  }
  .phone-tab--active {
    background: var(--day-chip-active);
    color: var(--day-chip-active-ink);
    font-weight: 600;
  }

  /* A SCREEN THAT FILLS THE BAR MUST NOT ALSO DRAW ITS DESKTOP HEADER.
     «Phone» names this exact failure: "A screen that tries to draw its own
     phone masthead ends up with the desktop header stacked above the
     shell's". Only the PAGE HEADER collapses -- see the next rule for why
     the controls row does not. */
  .app--phone-bar .page-header { display: none; }

  /* THE CONTROLS ROW STAYS. It used to be hidden here alongside the page
     header, and that removed the ONLY period navigation Month, Year and
     Agenda have -- a phone owner could not change month, change year, or
     page the agenda back a week (review finding; «Phone» asks for the row
     to WRAP, never to be deleted). What is hidden is only the DESKTOP view
     switcher inside it, because the phone tabs in the band above are the
     same control drawn for this width; .tl-nav keeps its prev/next/«اليوم».
     Scoped to the direct child so the range tabs in .tl-subcontrols -- a
     different .tabs group -- are untouched. */
  .app--phone-bar .tl-controls > .tabs { display: none; }
  .app--phone-bar .tl-controls { justify-content: center; }
  .app--phone-bar .tl-controls__spacer { display: none; }

  /* MOBILESHELL'S CONTROLS BLOCK (product-owner ruling, 19 Sep 2026). The
     rules this replaces built «Phone»'s two-row wrap from its prose -- "range
     plus search on the first, المتاح اليوم on the second" -- and left
     their own note saying the legend was NOT collapsed behind «مفتاح
     الحالات» as MobileShell draws it. The ruling settles the whole block
     against the artboard, and the collapse is now built (see `.cal-legend` in
     calendar.css).

     The layout of the block itself is `.cal-band`, also in calendar.css,
     because it is a calendar component; what stays here is only what belongs
     to the SHELL's phone treatment -- which of the screen's own controls the
     phone bar above has already taken over. */
  .app--phone-bar .tl-subcontrols { flex-wrap: wrap; }
  .app--phone-bar .tl-subcontrols__spacer { display: none; }

  /* NO SEARCH FIELD IN THE BAND (the same ruling). MobileShell draws a 44px
     search ICON in the density row; the ruling's own enumeration of the band
     omits it and says in words that "there is NO search field in the band".
     The consequence -- that search is then unreachable at phone width,
     because no filter artboard draws one either -- is recorded in HANDOFF §8
     rather than solved by inventing a control. */
  .app--phone-bar .cal-toolbar-search { display: none; }
}

/* The iCal rotation confirm on the channel-sync screen: the app's own
   reveal pattern, not a native dialog. Laid out as a block beneath the
   trigger so the warning reads before the two buttons, and the buttons sit
   on one row. */
.sync-rotate {
  display: block;
  flex-basis: 100%;
  margin-block-start: var(--sp-12);
  text-align: start;
}

.sync-rotate__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-8);
  margin-block-start: var(--sp-10);
}

/* The sidebar's needs-action count badge (R6, 21 Sep 2026). Same ground and
   ink as `.site-nav__soon` directly above -- they are the same kind of mark
   on the same row, and giving the count its own colour would make a number
   look like a state. Zero is not rendered at all, so this never paints a
   badge that says nothing. */
.site-nav__badge {
  margin-inline-start: auto;
  min-inline-size: 20px;
  padding: 1px 6px;
  border-radius: 5px;
  background: var(--sidebar-badge);
  color: var(--sidebar-text);
  font-size: var(--fs-11);
  font-weight: 700;
  text-align: center;
}

/* ==========================================================================
   THE DESKTOP TOP BAR (docs/design/SHELL_SPEC.md §1)
   56px, on every desktop screen, owned by templates/base.html. Drawn once,
   from `calgen.topbar()`.

   Values are the artboard's own, with ONE measured divergence, below.
   ========================================================================== */

.appbar {
  /* THE NOTCH, and this is the half of `viewport-fit=cover` that the round's
     brief said was only needed "where fixed". The brief's premise was wrong
     and it is worth recording why: `cover` extends the layout viewport into
     the unsafe areas regardless of how anything is positioned, so an IN-FLOW
     top bar slides under the status bar and the notch just as readily as a
     fixed one would. This bar is in flow -- it has no `position` at all, it
     is simply `.app`'s first child.

     Grown rather than merely padded, so the bar's own `--surface` paints the
     notch strip instead of leaving the page ground showing through it.
     `box-sizing: border-box` is global (line 18), so the content box stays
     exactly `--appbar-height`. */
  block-size: calc(var(--appbar-height) + var(--safe-area-top));
  padding-block-start: var(--safe-area-top);
  flex-shrink: 0;
  /* `1fr auto 1fr` is the artboard's own grid: it centres the search field
     on the PAGE rather than in whatever gap the end controls leave, so the
     field does not shift when a badge appears or a name gets longer. */
  display: grid;
  /* `minmax(0, 1fr)`, not `1fr`. A bare `1fr` has an automatic MINIMUM of
     its content, so the end cluster -- globe, bell, divider, 32px avatar,
     name, chevron -- could not shrink and pushed itself off the page
     instead.

     MEASURED DEFECT, 8 Oct 2026: at 1024px the account name collapsed to
     width 0 while the avatar sat 4px past the page edge, and at 768px the
     same cluster was 13-22px over. The grid now lets both side tracks go to
     zero, and the search field below shrinks with them. */
  /* `minmax(auto, 1fr)` ON THE END TRACK, and that asymmetry is the point.
     All three tracks were `minmax(0, 1fr) auto minmax(0, 1fr)`, which
     centres the search field perfectly -- and lets the END track shrink
     below the width its own contents need. The cluster's min-content is
     about 174px (globe, bell, divider, 32px avatar, chevron, with the name
     already ellipsised to nothing); at a 1024px laptop its equal share is
     122px, so it sat 4px past the page edge, and 8px at 1100 with a longer
     account name.
     `auto` as the MINIMUM means that track will not go under its
     min-content. The START track is empty -- it exists only to centre the
     field -- so it absorbs the difference, which is exactly the right thing
     to give up: above ~1150px both side tracks are equal and the field is
     centred as drawn, and below it the field drifts a few pixels toward the
     start instead of the bar overflowing the page.
     MEASURED at 375/768/1024/1100/1400 after the change: scrollWidth ==
     clientWidth at all five, on the two long-name fixtures that found it. */
  /* BOTH SIDE TRACKS ARE `minmax(auto, 1fr)` AS OF 15 OCT 2026, and the
     start one changed because it stopped being empty. The brand moved into
     this bar from the sidebar, so the start cell now has a min-content of
     its own -- the 34px glyph plus «مُدار» -- and `minmax(0, 1fr)` would
     let the track shrink under it and push the wordmark out of the bar at
     exactly the laptop widths the end track's own comment is about.
     The field below is `min(460px, 100%)`, so it is what gives way when
     both clusters need their room. */
  grid-template-columns: minmax(auto, 1fr) auto minmax(auto, 1fr);
  align-items: center;
  gap: var(--sp-16);
  padding-inline: var(--sp-20);
  background: var(--surface);
  border-block-end: 1px solid var(--border);
}

/* The start cell. Empty on a desktop -- it exists only to centre the search
   field -- and on a phone it carries the ☰, the screen's title, the wordmark
   and the screen's own action tiles. */
.appbar__start {
  display: flex;
  align-items: center;
  gap: 9px;
  min-inline-size: 0;
}

/* ---- WHAT THE BAR IS NOT ON A DESKTOP ------------------------------------

   `.appbar { display: none; }` USED TO BE HERE and is gone with the two-bar
   shell (14 Oct 2026). There is one bar now and it renders at every width;
   what changes below 1024px is its appearance, not its existence. The rule
   this replaces had its own hard-won history -- it was once written 834
   lines above `.appbar`'s definition, lost the cascade at equal specificity,
   and the desktop bar rendered on every phone with its end cluster 161px off
   the page. One bar removes the whole class of mistake: there is no second
   rule that has to be exactly complementary to this one.

   These four are the phone bar's parts, inert above 1023px. The screen-
   supplied `.topbar__text` and `.topbar__actions` are already default-hidden
   further down, scoped to 639px by the blueprint's «Phone» section.

   `.appbar .wordmark` LEFT THIS LIST ON 15 OCT 2026. The bar spans the whole
   viewport now and the sidebar has no header, so the brand is this bar's at
   every width -- one wordmark in the document instead of a desktop copy and
   a phone copy. The ☰ stays phone-only: there is no drawer to open above
   1023px. */
.appbar .topbar__btn { display: none; }

/* THE BRAND IN THE BAR. The element is `.wordmark`, shared with the sign-in
   and password screens' brand panel, so everything here is scoped to the bar
   and nothing edits the shared rules.

   `--ink`, NOT the base rule's `--on-dark`. Every other painter of
   `.wordmark__name` is a DARK surface -- the sidebar it just left, and the
   auth panel -- and the desktop bar is `--surface`, where `--on-dark` is a
   near-white word on a near-white ground. MEASURED on `--surface` with
   docs/design/contrast_check.py: --ink is 16.58:1 light and 17.47:1 dark;
   --on-dark is 1.02:1 light -- a word you cannot see at all. The
   1023px block below puts `--on-dark` back, because down there the bar IS
   the dark one. */
.appbar .wordmark {
  flex-shrink: 0;
  padding-inline: 0;
  padding-block-end: 0;
}
.appbar .wordmark__name { color: var(--ink); }



/* ---- the global search trigger ---- */

.appbar__search {
  /* 460px is the artboard's value and it is a MAXIMUM here, not a fixed
     width: the bar must survive a 1024px laptop, where 460px plus the end
     cluster does not fit the content column. Above ~1100px it renders at
     exactly the drawn 460. */
  inline-size: min(460px, 100%);
  block-size: 36px;
  display: flex;
  align-items: center;
  gap: 9px;
  padding-inline: var(--sp-12);
  border-radius: 9px;
  border: 1px solid var(--border);
  background: var(--chip);
  cursor: pointer;
  text-align: start;
}
.appbar__search:hover { border-color: var(--muted-2); }

/* TOUCH, ANY WIDTH (15 Oct 2026 device-matrix round). 36px is the
   artboard's own value on a mouse; the owner's 40px tap-target ruling
   wins for touch. Hit area only, via the negative-inset pseudo idiom
   `core/device_probes.py` detects -- the chip's paint, border and
   placeholder stay exactly as drawn. */
@media (pointer: coarse) {
  .appbar__search { position: relative; }
  .appbar__search::after {
    content: "";
    position: absolute;
    /* -3px, NOT -2 (measured). The pseudo's containing block is this
       button's PADDING edge, one px inside its own 1px border, so
       `inset: -2px` only cleared the border edge by 1px per side
       (measured 38x38 from a 36px box). -3px accounts for the border. */
    inset: -3px;
  }
}

/* THE ONE MEASURED DIVERGENCE FROM THE ARTBOARD ON THIS BAR.
   `calgen.topbar()` paints the placeholder and the key chip in its MUTED,
   #8A938B. Measured on the grounds it is actually painted on:
   3.17:1 on --surface and 2.89:1 on --chip, which is this field's own fill.
   Both fail 1.4.3's 4.5:1, and it is 12.5px text.

   #8A938B is the SAME grey docs/DESIGN_SYSTEM.md §2.5 already caught and
   replaced across the palette -- it is one of the seven that were failing
   when §2.5 claimed AA it had never measured. --muted is this project's
   recorded answer for it: 5.13:1 on --surface, 4.68:1 on --chip, 6.34:1 and
   5.68:1 in dark. So the resolution is not new here, it is the standing one
   applied to a new surface. */
.appbar__search-text {
  flex: 1;
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12.5px;
  color: var(--muted);
}
.appbar__search .icon { color: var(--muted); flex-shrink: 0; }

/* The key chip. LTR and isolated -- the artboard's own treatment, and
   README rule 2's real case: Latin text inside an RTL bar. */
.appbar__kbd {
  flex-shrink: 0;
  font-size: 10.5px;
  color: var(--muted);
  border: 1px solid var(--border);
  background: var(--surface);
  border-radius: 5px;
  padding: 1px 7px;
  direction: ltr;
  unicode-bidi: isolate;
  letter-spacing: 0.03em;
}

/* ---- the end cluster: globe, bell, divider, account ---- */

.appbar__end {
  display: flex;
  align-items: center;
  gap: var(--sp-6);
  justify-content: flex-end;
  /* NO `min-inline-size: 0` HERE, deliberately, and it took a measurement to
     see why. Setting it cancels this grid item's AUTOMATIC MINIMUM, which is
     exactly what `minmax(auto, 1fr)` on its track depends on -- so the track
     silently resolved back to `minmax(0, 1fr)` and the cluster went on
     overflowing by 4px at 1024 and 8px at 1100 after the track was
     "fixed". The name inside still carries its own min-inline-size: 0, so
     it is the thing that ellipsises; the cluster as a whole must not. */
}

.appbar__icon-btn {
  position: relative;
  inline-size: 36px;
  block-size: 36px;
  flex-shrink: 0;
  border-radius: 9px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted-2);
  text-decoration: none;
}
/* "No borders at rest -- the bar stays quiet" (SHELL_SPEC §1). The hover
   fill is the only thing that appears. */
.appbar__icon-btn:hover { background: var(--chip); color: var(--ink); }

/* The disabled globe: the control plus its reason, which is TEXT beside it
   and never a tooltip (README rule 7). */
.appbar__disabled {
  display: flex;
  align-items: center;
  gap: var(--sp-6);
}
.appbar__icon-btn--off { color: var(--faint); cursor: default; }
.appbar__icon-btn--off:hover { background: none; color: var(--faint); }

/* TOUCH, ANY WIDTH (15 Oct 2026 device-matrix round). 36px square is the
   artboard's own value on a mouse; the owner's 40px tap-target ruling
   wins for touch. `.appbar__icon-btn` already carries `position: relative`,
   so this is purely the negative-inset pseudo idiom -- the glyph and its
   9px-radius square stay exactly as drawn. */
@media (pointer: coarse) {
  .appbar__icon-btn::after {
    content: "";
    position: absolute;
    inset: -2px;
  }
}
.appbar__disabled-why {
  font-size: var(--fs-11);
  color: var(--muted);
  white-space: nowrap;
}

/* The bell's unread count. A MEASURED PAIR of tokens -- see tokens.css for
   why the dark numeral is dark ink rather than the drawn white. */
.appbar__count {
  position: absolute;
  inset-block-start: 3px;
  inset-inline-end: 2px;
  min-inline-size: 16px;
  block-size: 16px;
  border-radius: var(--radius-pill);
  background: var(--badge-urgent);
  color: var(--badge-urgent-text);
  font-size: 10px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  padding-inline: var(--sp-4);
}

.appbar__divider {
  inline-size: 1px;
  block-size: 22px;
  flex-shrink: 0;
  background: var(--border);
  margin-inline: var(--sp-8);
}

.appbar__account {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: var(--sp-4) var(--sp-6);
  border-radius: 9px;
  color: inherit;
  cursor: pointer;
  min-inline-size: 0;
}
.appbar__account:hover { background: var(--chip); }

/* 32px and fully round here -- the bar's avatar, not the sidebar's 8px
   square. The artboard draws both and they are different controls. */
.appbar__avatar {
  inline-size: 32px;
  block-size: 32px;
  flex-shrink: 0;
  border-radius: var(--radius-pill);
  background: var(--avatar-bg);
  color: var(--avatar-text);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: var(--fs-13);
}
.appbar__account-name {
  font-size: var(--fs-13);
  font-weight: 600;
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.appbar__account .icon { color: var(--muted-2); flex-shrink: 0; }


/* ==========================================================================
   THE SAME BAR, AT OR BELOW 1023px -- `MobileShell.dc.html`'s dark app bar.

   THE PRODUCT OWNER'S SHELL-PARITY FINDING, 14 Oct 2026: "the phone layout
   still carries the OLD top bar". It did -- the phone had its own `<header
   class="topbar">` carrying ☰ and the wordmark and nothing else, so the
   bell, the notifications panel and the global search existed only above
   1024px. There was no way to reach a notification on a phone at all.

   WHAT SURVIVES FROM THE DESKTOP BAR: the search trigger (as a 44px icon
   tile) and the bell with its badge. WHAT DOES NOT: the globe and the
   account control, both of which `MobileDrawer.dc.html` draws in the drawer
   -- «موقع الحجز» as a nav row and the user block as «حسابك والاشتراك».
   Five shell tiles beside a screen's own two do not fit next to a title at
   360px, and the two the drawer already owns are the two that go.

   THE 640-1023px BAND is unchanged from before the merge: ☰ + wordmark, and
   now the search and the bell. `.topbar__text` / `.topbar__actions` stay
   scoped to 639px, so a screen keeps its own page header across that band --
   which is the band a wrongly-scoped media query broke once, and why 768px
   is a permanent probe width.
   ========================================================================== */
@media (max-width: 1023px) {
  .appbar {
    /* MobileShell: 10px 12px 11px, gap 9px, on the sidebar's own ground. */
    display: flex;
    align-items: center;
    gap: 9px;
    block-size: auto;
    padding: 10px 12px 11px;
    background: var(--sidebar);
    color: var(--on-dark);
    border-block-end: 0;
  }

  .appbar__start {
    flex: 1;
    min-inline-size: 0;
  }
  .appbar .topbar__btn { display: flex; }
  .appbar .wordmark { padding: 0; }
  /* THE BAR IS THE DARK ONE AGAIN AT THIS WIDTH, so the brand's name goes
     back to the token every other dark painter of it uses. Declared after
     the desktop `--ink` rule and at the same specificity, which is the only
     way it wins -- `@media` adds none. */
  .appbar .wordmark__name { color: var(--on-dark); }

  /* The ☰ / back control and the screen's own tiles: 44px squares at radius
     12 on `--sidebar-active`, with `--mast-icon` strokes. */
  .topbar__btn {
    inline-size: var(--control-height);
    block-size: var(--control-height);
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 12px;
    background: var(--sidebar-active);
    color: var(--mast-icon);
    cursor: pointer;
  }
  .topbar__btn--end { margin-inline-start: auto; }

  /* THE SEARCH TRIGGER BECOMES A TILE. Same button, same `paletteOpen()`,
     same panel -- the owner's "give the bar's search icon the same panel".
     Its placeholder and the Ctrl+K chip go: a key chip on a phone names a
     key that is not there. `aria-label` on the button carries the name. */
  .appbar__search {
    inline-size: var(--control-height);
    block-size: var(--control-height);
    flex: none;
    justify-content: center;
    gap: 0;
    padding-inline: 0;
    border: 0;
    border-radius: 12px;
    background: var(--sidebar-active);
    color: var(--mast-icon);
  }
  .appbar__search:hover { border-color: transparent; }
  .appbar__search-text,
  .appbar__kbd { display: none; }

  .appbar__end {
    flex: none;
    gap: 9px;
  }
  /* Both live in the drawer at this width -- see the block comment. */
  .appbar__globe,
  .appbar__divider,
  .appbar__account { display: none; }

  /* The bell, as the same dark tile as its neighbours. */
  .appbar .appbar__icon-btn {
    inline-size: var(--control-height);
    block-size: var(--control-height);
    border-radius: 12px;
    background: var(--sidebar-active);
    color: var(--mast-icon);
  }
}


/* ==========================================================================
   THE ACCOUNT MENU (SHELL_SPEC §2, artboard `DashboardAccount`)
   300px, 6px under the bar, at the inline-end, over a scrim.
   ========================================================================== */

.account-menu {
  position: fixed;
  inset: 0;
  z-index: 60;
}
.account-menu__scrim {
  position: absolute;
  inset: 0;
  border: 0;
  padding: 0;
  cursor: pointer;
  /* THE SHELL'S ONE SCRIM TECHNIQUE -- `--ink` at an opacity, never a fresh
     rgba(). The drawer, the calendar's filter sheet and the dashboard's
     modal all settled on this so the product carries ONE scrim; a literal
     rgb() would also be a hard-coded colour, which §1.3 forbids and which
     core/test_design_rules.py would NOT catch, because it scans for hex. */
  background: var(--ink);
  opacity: .34;
}
.account-menu__panel {
  position: absolute;
  inset-inline-end: var(--sp-16);
  /* The bar's own height plus the 6px the artboard anchors it by. */
  inset-block-start: calc(var(--appbar-height) + var(--sp-6));
  /* 320px, up from 300: the reference's rows carry their label and their
     control with real air between them, and at 300 the «العربية | English»
     pair and the word «اللغة» were nearly touching in English. */
  inline-size: 320px;
  background: var(--surface);
  border: 1px solid var(--border);
  /* --radius-xl, the reference's own soft card corner, replacing a one-off
     13px that matched nothing else in the token table. */
  border-radius: var(--radius-xl);
  overflow: hidden;
  /* NO box-shadow. The artboard draws one; §1's "no shadows anywhere" wins
     over the artboard for popovers, which is the tiebreak this project
     already recorded for the calendar's and the dashboard's overlays. The
     1px border is what separates the panel from the page.

     AND NOT TRANSLUCENT, though the owner's reference asks for a translucent
     dark ground. Declined deliberately and recorded in docs/HANDOFF.md §8:
     `core/test_design_rules.py` measures every foreground token against the
     GROUND TOKEN it is painted on, and a panel painted at some fraction of
     --surface has no ground token -- its real ground is whatever the page
     behind the scrim happens to be, which differs per screen and per scroll
     position. Every contrast number in this panel would become unverifiable
     to buy an effect that only exists in the reference's dark theme anyway;
     the product has a light one, where "translucent dark" is not a thing a
     token swap can produce. */
}

/* THE HEAD, and it is the one place the reference keeps a rule under it.
   `align-items: center` against a two-line id block puts the avatar on the
   optical centre of name + e-mail, which is what the reference draws. */
.account-menu__head {
  display: flex;
  align-items: center;
  gap: var(--sp-12);
  padding: var(--sp-18);
  border-block-end: 1px solid var(--line);
}
.account-menu__avatar {
  inline-size: 40px;
  block-size: 40px;
  flex-shrink: 0;
  border-radius: var(--radius-pill);
  background: var(--avatar-bg);
  color: var(--avatar-text);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: var(--fs-14);
}
.account-menu__id {
  display: flex;
  flex-direction: column;
  line-height: 1.4;
  min-inline-size: 0;
}
.account-menu__name {
  font-size: 13.5px;
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* LTR and isolated, aligned to the inline-end -- the artboard's own three
   declarations. A Latin e-mail in an RTL column. */
.account-menu__email {
  font-size: var(--fs-11);
  color: var(--muted);
  direction: ltr;
  unicode-bidi: isolate;
  text-align: end;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.account-menu__role {
  margin-inline-start: auto;
  flex-shrink: 0;
  font-size: 10.5px;
  font-weight: 600;
  color: var(--tag-text);
  background: var(--tag-bg);
  border-radius: var(--radius-pill);
  padding: 2px 9px;
}

/* NO PER-ROW RULE. The reference separates the head and the logout and
   nothing else; four hairlines across a five-row panel read as a table.
   What replaces them is block padding -- `--sp-12` against a 30px control
   makes a 54px row, which is the reference's own row rhythm. */
.account-menu__row {
  display: flex;
  align-items: center;
  gap: var(--sp-10);
  padding: var(--sp-12) var(--sp-18);
  /* EVERY ROW IS THE SAME HEIGHT, control or not. Without this the two
     plain-text links and the logout row -- which have no 30px pill to set
     their height -- came out 10px shorter than the two that do, and the
     panel read as two blocks of different rhythm rather than one list. */
  min-block-size: 54px;
  inline-size: 100%;
  color: inherit;
  text-decoration: none;
  background: none;
  text-align: start;
}
.account-menu__row--link:hover,
.account-menu__row--danger:hover { background: var(--chip); }
.account-menu__row--button { margin: 0; }
.account-menu__label {
  flex: 1;
  min-inline-size: 0;
  font-size: var(--fs-13);
  font-weight: 600;
}
.account-menu__row .icon { color: var(--muted-2); flex-shrink: 0; }

/* The theme cycle: glyph first, then the current theme's name. A 30px pill
   so it sits on the same baseline rhythm as the language pills opposite it,
   and `--chip` so the row's one pressable thing looks pressable without
   needing a border. */
.account-menu__cycle {
  display: flex;
  align-items: center;
  gap: var(--sp-6);
  background: var(--chip);
  border-radius: var(--radius-pill);
  block-size: 30px;
  padding-inline: var(--sp-12);
  color: inherit;
  cursor: pointer;
}
.account-menu__value { font-size: var(--fs-12); color: var(--muted-2); }

/* ---- the two FACT rows: «الحساب» and «اليوم» (15 Oct 2026) --------------
   The sidebar's header moved in here when the top bar took the brand. These
   are statements, not controls, so they get no hover fill and no pointer --
   `.account-menu__row--link:hover` deliberately does not name this modifier.

   SHORTER THAN A CONTROL ROW, and that is the whole modifier. `.account-
   menu__row`'s `min-block-size: 54px` exists so a plain-text link lines up
   with a row carrying a 30px pill; a fact has nothing to line up with, and
   two 54px rows of read-only text pushed «تسجيل الخروج» off a 667px phone's
   screen when the panel opened. 40px is the same rhythm at a label's own
   height. */
.account-menu__row--fact {
  min-block-size: 40px;
  padding-block: var(--sp-8);
  cursor: default;
}

/* Both values sit at the inline-end against their label, and both ellipsise:
   an Organization name is free text an owner typed and «شاليهات الموج
   للضيافة والاستجمام» is not a strange one. `--muted-2` is this panel's
   existing value colour (`.account-menu__value` above), so a fact reads as
   secondary to the label beside it rather than as a second heading. */
.account-menu__org,
.account-menu__today {
  flex-shrink: 1;
  min-inline-size: 0;
  font-size: var(--fs-12);
  color: var(--muted-2);
  text-align: end;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.account-menu__today { letter-spacing: 0.3px; }

/* «قريبًا» beside the disabled English option -- the reason, in text. */
.account-menu__soon {
  flex-shrink: 0;
  font-size: var(--fs-11);
  font-weight: 600;
  color: var(--muted);
}

/* THE SECOND AND LAST RULE IN THE PANEL. It lives on the logout FORM rather
   than on the button, because the form is the element that always exists --
   a rule on `.account-menu__row--danger` would have to be re-stated anywhere
   else a danger row is used. */
.account-menu__out {
  display: block;
  border-block-start: 1px solid var(--line);
}
.account-menu__row--danger {
  cursor: pointer;
  /* --danger-text (7.88:1 on --surface), not the artboard's #B42318
     (6.57:1). Both clear AA; this product already paints every piece of
     danger text with this one token, and a second near-identical red is how
     a palette starts to rot. Recorded in docs/HANDOFF.md. */
  color: var(--danger-text);
}
.account-menu__row--danger .icon,
.account-menu__row--danger .account-menu__label { color: var(--danger-text); }

/* ---- the segmented control the language row uses ---- */

.segmented {
  display: flex;
  gap: var(--sp-2);
  background: var(--chip);
  padding: var(--sp-2);
  border-radius: var(--radius-sm);
  flex-shrink: 0;
}
.segmented__opt {
  block-size: 26px;
  display: flex;
  align-items: center;
  padding-inline: 11px;
  border-radius: var(--radius-xs);
  font-size: 11.5px;
  color: var(--muted-2);
  background: none;
}
.segmented__opt--on {
  background: var(--inverse-surface);
  color: var(--on-inverse);
  font-weight: 600;
}
/* A disabled option is INACTIVE, which 1.4.3 exempts from the text
   threshold -- and its reason is the «قريبًا» text beside the control, not
   this colour. It still has to look unavailable rather than merely quiet,
   which is what the cursor and the opacity say together. */
.segmented__opt[disabled] {
  color: var(--faint);
  cursor: not-allowed;
}

/* TOUCH, ANY WIDTH (15 Oct 2026 device-matrix round). 26px is the artboard's
   own density for this control (also the guests list's filter segmented) on
   a mouse. The owner's 40px tap-target ruling is scoped to touch and wins
   there -- this is the divergence CLAUDE.md asks to record rather than
   leave silent, kept narrow to the input that actually needs it. Grown as
   PAINT, not just hit area, because this is a filled pill the reader reads
   the edge of -- an invisible overflow here would make clicking "beside"
   the pill select it, which the icon-button idiom used elsewhere does not
   risk. */
@media (pointer: coarse) {
  .segmented__opt { min-block-size: 40px; }
}

/* ---- `--outline`: the account menu's language pair -------------------------
   The owner's reference (03-1b) draws the chosen language as an OUTLINED
   capsule and the other as plain text -- no container fill, no filled
   segment. A MODIFIER rather than a change to `.segmented` itself, because
   the base class is also the guests list's filter control and restyling it
   here would have restyled a screen nobody in this round looked at.

   The outline is `--ink` -- the same token the chosen label is lettered in,
   so the capsule and its word are one mark. `--muted-2` was tried first and
   put side by side with the reference: it reads as a disabled field rather
   than as the live choice, because the reference's outline is as bright as
   its text and a grey one is not. Being a token, it swaps: near-white on the
   dark panel, near-black on the light one.                                  */
.segmented--outline {
  background: none;
  padding: 0;
  gap: var(--sp-4);
}
.segmented--outline .segmented__opt {
  block-size: 30px;
  padding-inline: var(--sp-12);
  border-radius: var(--radius-pill);
  font-size: var(--fs-12);
}
.segmented--outline .segmented__opt--on {
  background: none;
  color: var(--ink);
  border: 1px solid var(--ink);
}


/* ==========================================================================
   THE COMMAND PALETTE (SHELL_SPEC §1)
   ========================================================================== */

.palette {
  position: fixed;
  inset: 0;
  z-index: 70;
}
.palette__scrim {
  position: absolute;
  inset: 0;
  border: 0;
  padding: 0;
  cursor: pointer;
  /* THE SHELL'S ONE SCRIM TECHNIQUE -- `--ink` at an opacity, never a fresh
     rgba(). The drawer, the calendar's filter sheet and the dashboard's
     modal all settled on this so the product carries ONE scrim; a literal
     rgb() would also be a hard-coded colour, which §1.3 forbids and which
     core/test_design_rules.py would NOT catch, because it scans for hex. */
  background: var(--ink);
  opacity: .34;
  /* THE BLUR IS ON THE SCRIM, NOT ON THE PANEL -- and that is a deliberate
     departure from the owner's reference 05-1b, which draws a TRANSLUCENT
     panel (12 Oct 2026; recorded in docs/HANDOFF.md §8).

     A translucent panel changes the ground every word inside it is painted
     on, by however much of the page shows through. This product measures
     every foreground token against every ground it is painted on
     (docs/design/contrast_check.py), and `--muted` clears AA on `--surface`
     by a margin thin enough that a few per cent of a darkened page behind
     it is not obviously safe -- it would be a claim nothing here can check,
     on the exact token whose unmeasured claim this project has already had
     to correct once (§2.5). Blurring what is BEHIND the panel produces the
     reference's softness where the reference's softness actually reads, and
     costs no contrast at all: the panel stays an opaque `--surface`. */
  backdrop-filter: blur(3px);
}
.palette__panel {
  position: absolute;
  inset-inline: 0;
  /* 10vh, from 12vh: the panel is taller now that it answers before anyone
     types, and the reference sits its panel high on the page. */
  /* dvh: the panel is positioned a tenth of the way down THE VISIBLE
     viewport, which on a phone with a browser toolbar is not the same tenth
     `vh` would pick. */
  inset-block-start: 10vh;
  inset-block-start: 10dvh;
  /* 640px, from 560px (05-1b). The resting state draws an icon, a name and
     for «آخر الإجراءات» a unit/guest line and a time on one row; at 560px
     the sub-line truncated on the longest real unit names. */
  inline-size: min(640px, calc(100% - 2 * var(--sp-16)));
  margin-inline: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  overflow: hidden;
  /* No shadow, for the same reason as the account menu. */
}

.palette__field {
  display: flex;
  align-items: center;
  gap: var(--sp-12);
  /* THE LARGE INPUT (05-1b). 20px of block padding around a 17px line makes
     the field ~60px tall, against the 46px it was -- the reference's search
     row is the panel's biggest element and the thing a reader aims at. */
  padding: var(--sp-20) var(--sp-20);
  border-block-end: 1px solid var(--line);
}
.palette__field .icon { color: var(--muted); flex-shrink: 0; }
.palette__input {
  flex: 1;
  min-inline-size: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--fs-17);
  color: var(--ink);
  padding: 0;
}
.palette__input::placeholder { color: var(--muted); }
/* WebKit draws its own clear button inside a search input; the panel has
   Esc and a scrim, and a second clear affordance inside the field reads as
   a control the design did not put there. */
.palette__input::-webkit-search-cancel-button { display: none; }
.palette__kbd {
  flex-shrink: 0;
  font-size: 10.5px;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 1px 7px;
  direction: ltr;
  unicode-bidi: isolate;
}

/* ---- the filter chips (05-1b) ----------------------------------------- */

.palette__chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-6);
  padding: var(--sp-12) var(--sp-20);
}
/* THE SAME CAPSULE THE ACCOUNT MENU'S LANGUAGE PAIR USES
   (`.segmented--outline`, §4): an outlined pill for the chosen one, a quiet
   one for the rest. Not a filled chip, and not `--primary` on
   `--on-primary`: in dark mode `--ink` and `--on-primary` are the same
   colour, which is how a filled active tab once shipped at 1.00:1. */
.palette__chip {
  block-size: 30px;
  padding-inline: var(--sp-12);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: none;
  font: inherit;
  font-size: var(--fs-12);
  color: var(--muted-2);
  cursor: pointer;
}
.palette__chip:hover { color: var(--ink); }
.palette__chip--on {
  border-color: var(--ink);
  color: var(--ink);
  font-weight: 600;
}

/* ---- the body: what HTMX swaps, and the list that scrolls inside it ---- */

.palette__body { display: block; }
/* The open-state placeholder. `.skeleton`'s own block padding would double
   the panel's, so this trims it to the row rhythm the real list has. */
.palette__skeleton { padding: var(--sp-12) var(--sp-20); }

.palette__results {
  /* THE LIST SCROLLS INSIDE THE PANEL (05-1b), which is why the chips are
     outside this element: they stay put while the groups move under them. */
  max-block-size: min(52vh, 420px);
  max-block-size: min(52dvh, 420px);
  overflow-y: auto;
  padding-block: var(--sp-6) var(--sp-10);
}
.palette__group { padding-block-end: var(--sp-6); }
.palette__group-label {
  padding: var(--sp-6) var(--sp-20);
  font-size: var(--fs-11);
  font-weight: 600;
  color: var(--muted);
}
.palette__item {
  display: flex;
  /* `center`, from `baseline`: a row carries an icon now, and an svg has no
     baseline worth aligning a word to. */
  align-items: center;
  gap: var(--sp-10);
  /* 44px, like every other navigation row (README rule 6) -- the palette is
     reachable on a phone too. */
  min-block-size: var(--nav-item-height);
  padding: var(--sp-6) var(--sp-20);
  color: inherit;
  text-decoration: none;
}
.palette__item:hover,
.palette__item:focus-visible { background: var(--chip); }
.palette__item-icon {
  display: flex;
  flex-shrink: 0;
  color: var(--muted-2);
}
.palette__item:hover .palette__item-icon { color: var(--ink); }
.palette__item-title {
  font-size: var(--fs-13);
  font-weight: 600;
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.palette__item-meta {
  font-size: var(--fs-12);
  color: var(--muted);
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* ONLY WHEN IT IS THE LAST THING IN THE ROW. «آخر الإجراءات» puts a time
   after the sub-line, and two `auto` inline-start margins in one flex row
   SPLIT the free space between them instead of pushing one element to the
   end -- which parked the unit/guest line in the middle of the row. */
.palette__item-meta:last-child {
  margin-inline-start: auto;
  flex-shrink: 0;
}
.palette__item-when {
  margin-inline-start: auto;
  padding-inline-start: var(--sp-10);
  flex-shrink: 0;
  font-size: var(--fs-11);
  color: var(--muted);
}
.palette__hint {
  margin: 0;
  padding: var(--sp-16) var(--sp-20);
  font-size: var(--fs-13);
  color: var(--muted);
}


/* ==========================================================================
   THE COLLAPSE HANDLE AND THE 72px ICON RAIL (SHELL_SPEC §2b)
   ========================================================================== */

.sidebar__collapse { margin: 0; }
.sidebar__collapse-btn {
  position: absolute;
  /* On the sidebar's CONTENT edge, half outside it, which is where the
     artboard puts it. */
  inset-inline-end: -12px;
  inset-block-start: 50%;
  margin-block-start: -12px;
  inline-size: 24px;
  block-size: 24px;
  border-radius: var(--radius-pill);
  background: var(--sidebar-active);
  border: 1px solid var(--sidebar-line);
  color: var(--sidebar-accent);
  display: flex;
  align-items: center;
  justify-content: center;
  /* 1, NOT 5 (package Issue 12: "the sidebar collapse handle no longer
     stacks above overlays -- found when the wizard modal crossed the
     sidebar edge").

     The handle sits at `inset-inline-end: -12px`, half OUTSIDE the sidebar
     and therefore over the content column. The wizard modal centres over
     that column rather than the page, so its edge and this handle occupy
     the same pixels -- and at 5 the handle won against anything stacked
     below it. It only ever needs to clear the sidebar's own background,
     which 1 does. Every overlay in this product is 20 or above. */
  z-index: 1;
  cursor: pointer;
}

/* TOUCH, ANY WIDTH (15 Oct 2026 device-matrix round) -- and this one is NOT
   an unstyled-default bug like `.drawer-close` turned out to be: the handle
   IS a designed 24px circle, and `.sidebar` renders above the 1023px
   phone-shell breakpoint, where a mouse is the normal input. But a
   touchscreen at that width is real (`ipad_landscape` is 1194px AND
   `has_touch: true`, and the matrix found this control there) -- a thumb
   can reach the desktop sidebar, so it owes the 40px floor same as any
   other reachable control. Hit area only, via the negative-inset pseudo
   idiom: the 24px circle keeps its exact paint and position; only what a
   touch can land on grows. */
@media (pointer: coarse) {
  .sidebar__collapse-btn::after {
    content: "";
    position: absolute;
    /* -9px, NOT -8 (measured). The pseudo's containing block is this
       button's PADDING edge, one px inside its own 1px border, so
       `inset: -8px` only cleared the border edge by 7px per side
       (measured 38x38 from a 24px box). -9px accounts for the border. */
    inset: -9px;
  }
}

/* ---- the rail ---- */

.app--rail .sidebar {
  inline-size: var(--sidebar-rail-width);
  /* The rail is narrow enough that the 14px side padding would leave almost
     nothing for a 44px row to centre in. */
  padding-inline: var(--sp-10);
}
/* The glyph points at the sidebar's edge ("fold this away"); folded, it has
   to point back out. One glyph, rotated -- never a second path. */
.app--rail .sidebar__collapse-btn .icon { transform: rotate(180deg); }

/* "Labels become tooltips" (§2b). The `title` on each row is what the
   tooltip is; `.site-nav__label` is what goes. */
/* `.wordmark__text` AND `.sidebar__date` CAME OUT OF THIS LIST, 15 Oct
   2026, AND LEAVING THEM WOULD HAVE BEEN A REAL DEFECT rather than dead
   weight. `app--rail` sits on `.app`, which now CONTAINS the top bar -- so
   `.app--rail .wordmark__text` would have hidden the product's name in the
   bar for every reader who has ever folded the sidebar, on every screen,
   at every width. The same trap caught `.app--rail .drawer-account` once
   already; see the block below it. */
.app--rail .site-nav__label,
.app--rail .site-nav__chev,
.app--rail .site-nav__children,
.app--rail .drawer-account { display: none; }

/* THE RAIL IS A DESKTOP STATE AND MUST NOT REACH THE DRAWER (14 Oct 2026).

   `app--rail` is persisted per user (`UserPreference.sidebar_collapsed`), so
   it is on the shell at EVERY width for anyone who has ever folded the
   sidebar on a laptop. The 1023px block already neutralises the rail's width
   and padding for exactly that reason — its own comment says "otherwise a
   reader who folded the sidebar on a laptop would open the phone and find a
   72px drawer with no labels" — and this row was missed, because it is 1,700
   lines away from that block and declared AFTER it.

   WHAT IT COST: on a phone, for any such owner, the drawer had **no account
   block at all** — no language, no appearance, no account link, no sign-out.
   And that is the drawer this round just made the phone's only route to all
   four, so the two faults compound. Found by driving the drawer open in
   `_review/mobile-night/overlays.py` and getting NOTOPEN on a correct phone
   bar.

   Written here rather than in the 1023px block above, and the position is
   the fix: `@media` adds no specificity and both selectors are (0,2,0), so
   an override declared before the rule it overrides does nothing. */
@media (max-width: 1023px) {
  .app--rail .drawer-account { display: flex; }
}


/* The active pill and the badges SURVIVE the fold (§2b, stated). The pill
   becomes a square and the badge a dot on the row's corner, because a
   count pill cannot sit beside a label that is no longer there. */
.app--rail .site-nav__link,
.app--rail .site-nav__child {
  justify-content: center;
  padding-inline: 0;
}
.app--rail .site-nav__badge {
  position: absolute;
  inset-block-start: 4px;
  inset-inline-end: 4px;
  margin-inline-start: 0;
  min-inline-size: 16px;
  padding: 0 var(--sp-4);
  text-align: center;
}
/* `.app--rail .wordmark` WAS HERE and is gone for the reason above: the
   only `.wordmark` inside `.app` is the TOP BAR's now, and centring the
   brand in a 1400px bar because the sidebar is folded is nonsense. The
   sidebar has no wordmark to fold. */


/* The pricing engine's per-night figures, under the manual booking form's
   nightly-rate field (merge round, 8 Oct 2026).

   Shown ONLY when the engine priced the nights differently and the single
   rate field was therefore left empty -- see the view's docstring for why a
   wrong single number is worse than none. It is a short list a reader scans
   while typing a rate, not a table: no header, no borders, and the date and
   the amount at opposite ends of the row so the amounts form a column. */
.price-nights {
  margin: var(--sp-6) 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.price-nights__row {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-12);
  font-size: var(--fs-12);
  color: var(--muted);
}

/* ===========================================================================
   THE LOCKED SHELL — an organization whose seven setup steps are not all
   done (ONBOARDING_SPEC §0, owner ruling 13 Oct 2026).

   IN base.css AND NOT IN onboarding.css, because it is the SHELL's own
   locked variant rather than one screen's layout. The flow delegates into
   Unit Configuration for a slot unit's periods, so that screen renders this
   sidebar too — and a screen that does not link onboarding.css would draw
   the whole sidebar with no styles at all.
   =========================================================================== */

/* --- the locked sidebar's progress card --------------------------------- */

/* Where the date line usually sits (ONBOARDING_SPEC §0). The sidebar stays
   VISIBLE and every row is dimmed with a lock — showing what the account
   becomes is the argument for finishing it — and there are NO COUNT BADGES,
   because there are no counts before activation and a drawn count that
   cannot exist is a lie. */
.onb-navcard {
  margin-block-end: var(--sp-14);
  padding: 13px var(--sp-14);
  border-radius: var(--radius-lg);
  background: var(--sidebar-active);
}
.onb-navcard__row {
  display: flex;
  align-items: baseline;
  gap: var(--sp-8);
}
.onb-navcard__title {
  flex: 1;
  color: var(--on-dark);
  font-size: 13px;
  font-weight: 700;
}
.onb-navcard__count {
  color: var(--on-dark);
  font-size: 13px;
  font-weight: 700;
}
.onb-navcard__track {
  display: flex;
  height: 5px;
  margin-block-start: 9px;
  border-radius: var(--radius-pill);
  background: var(--sidebar);
}
.onb-navcard__fill {
  height: 5px;
  border-radius: var(--radius-pill);
  background: var(--sidebar-progress);
}
.onb-navcard__note {
  margin-block-start: var(--sp-8);
  color: var(--sidebar-text);
  font-size: var(--fs-11);
}

/* The dimmed nav row. A <span>, never an <a>: 45% opacity on a live link is
   a control that looks disabled and still navigates, which is worse than
   either. The lock glyph takes the badge's place. */
.onb-navrow {
  display: flex;
  align-items: center;
  gap: var(--sp-12);
  height: 44px;
  padding-inline: var(--sp-12);
  border-radius: var(--radius-sm);
  color: var(--sidebar-text);
  opacity: 0.45;
}
.onb-navrow__label {
  font-size: 13.5px;
}
.onb-navrow__lock {
  display: flex;
  margin-inline-start: auto;
}

/* ==========================================================================
   TOUCH TARGETS (device-matrix round, 15 Oct 2026)

   `@media (pointer: coarse)` and NOT a width breakpoint, which is the whole
   point: a target's required size follows the INPUT DEVICE, not the viewport.
   The owner's 40px floor is scoped to touch profiles for that reason, and an
   iPad in landscape is 1194px wide -- the DESKTOP shell -- and still driven
   by a thumb. A width rule misses exactly that case, and it is the case the
   matrix found the most offenders in.

   Every mouse rendering is byte-for-byte unchanged, so no desktop artboard is
   contradicted. Where a phone artboard draws a smaller control, the owner's
   40px ruling is the newer and more specific instruction and wins for touch;
   recorded in docs/design/CHANGELOG.md rather than left unexplained.

   `min-block-size` beats an explicit `block-size` (the minimum wins over the
   used value), so these need no `!important` and no selector weight games.
   ========================================================================== */
@media (pointer: coarse) {
  /* The single biggest offender left on the board: `.tab` is `block-size:
     32px` and appears on the reservations, payments, units and dashboard
     screens -- every tab bar in the product. */
  .tab { min-block-size: 40px; }
}
