/* ==========================================================================
   Mawkeji admin panel — shared shell styles
   Built strictly from assets/css/tokens.css (ورشة البازلت / DESIGN.md).
   No component styling belongs in tokens.css; this file is the first
   consumer. Later phase-2+ screens add to this file, never fork a second one.
   ========================================================================== */

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

html {
  background: var(--surface-ground);
  color-scheme: light;
}

/* The document line-height is --leading-control (1.5), NOT --leading-body
   (1.8). --leading-body is sized for running Arabic prose with its ascenders
   and diacritics, and inheriting it document-wide was quietly inflating every
   single-line thing in the panel: list rows, chips, status pills, buttons,
   field labels. A 0.1em-padded chip at 1.8 is ~44px tall on its own; the same
   chip at 1.5 is ~36px, and a four-line tenant row loses ~30px of pure air.
   Prose opts BACK IN via .prose-leading below — the rule is "1.8 where
   sentences wrap", not "1.8 everywhere". */
body {
  margin: 0;
  min-height: 100dvh;
  background: var(--surface-ground);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--text-body);
  line-height: var(--leading-control);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Running text — helper copy under a field, empty-state explanations, the
   login lede, section descriptions. Anything that can wrap to a second line
   and is read as a sentence rather than scanned as a value. */
.prose-leading,
.auth-lede,
.auth-help,
.field-hint,
.items-empty-text,
.site-section-desc {
  line-height: var(--leading-body);
}

h1, h2, p, form { margin: 0; }

button, input {
  font-family: inherit;
  color: inherit;
}

/* Visible keyboard focus everywhere — DESIGN.md §6 */
:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   Shell — the centred single-column frame both login and the signed-in
   entry sit inside. Content spine caps at --content-measure (DESIGN.md §3).
   -------------------------------------------------------------------------- */

.shell {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: var(--space-6) var(--space-4);
}

.shell-inner {
  width: 100%;
  max-width: 24rem;
}

/* --------------------------------------------------------------------------
   Auth card (login screen) — the one true object on this screen, styled as
   a sheet: jass on hajar, hairline border, --shadow-sheet elevation.
   -------------------------------------------------------------------------- */

.auth-card {
  background: var(--surface-raised);
  border: var(--hairline);
  border-radius: var(--radius-surface);
  box-shadow: var(--shadow-sheet);
  padding: var(--space-6) var(--space-5);
}

.wordmark {
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--text-display);
  line-height: var(--leading-title);
  color: var(--ink);
  text-align: center;
}

.auth-lede {
  margin-block-start: var(--space-2);
  color: var(--ink-soft);
  text-align: center;
}

.auth-form {
  margin-block-start: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* A .field's own `display: flex` (author stylesheet) otherwise beats the
   UA stylesheet's `[hidden] { display: none }` at equal specificity — same
   footgun this file already guards against per-component below (.form-
   error[hidden], .field-error[hidden], …). new-tenant.html's conditional
   themeIdCustom field (page-new-tenant.js) is the first .field the panel
   ever toggles via the `hidden` IDL property rather than always rendering
   it, so this is the first place the gap actually surfaced — the rule is
   general on purpose, so any future conditionally-hidden .field is safe by
   default instead of needing its own one-off override. */
.field[hidden] { display: none; }

.field label {
  font-weight: var(--weight-bold);
  font-size: var(--text-body);
}

.field input {
  min-height: var(--control-height);
  padding-inline: var(--space-4);
  background: var(--surface-ground);
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  font-size: var(--text-body); /* 16px floor — never smaller, never zooms iOS */
}

/* Email / password values are inherently Latin-script even inside an
   RTL form: set the field itself LTR so "@" and digits shape correctly,
   right-aligned so it still reads as the start side of the row. */
.field-ltr input {
  direction: ltr;
  text-align: right;
}

.field input::placeholder {
  color: var(--ink-soft);
}

.field input:focus-visible {
  border-color: var(--kahraman-deep);
}

/* --------------------------------------------------------------------------
   Buttons
   -------------------------------------------------------------------------- */

/* .btn is worn by both <button> and <a> (the console's "عميل جديد", the
   items screen's add action). An anchor brings the UA underline with it, so
   a solid basalt button rendered underlined — reset here rather than at each
   anchor call site. display:inline-flex + centering for the same reason:
   an <a> has no button box model to inherit. */
.btn {
  min-height: var(--tap-min);
  padding-inline: var(--space-5);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: var(--radius-control);
  font-size: var(--text-body);
  font-weight: var(--weight-bold);
  line-height: var(--leading-control);
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--motion-quick) var(--ease-out),
              opacity var(--motion-quick) var(--ease-out);
}

/* Primary control color is basalt — DESIGN.md §1: no blue, color is state. */
.btn-primary {
  width: 100%;
  background: var(--basalt);
  color: var(--ink-on-inverse);
}

.btn-primary:hover:not(:disabled) {
  background: color-mix(in srgb, var(--basalt) 88%, black);
}

.btn-primary:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.btn-primary:focus-visible {
  outline: var(--focus-ring-on-dark);
  outline-offset: var(--focus-ring-offset);
}

.btn-secondary {
  background: transparent;
  color: var(--ink);
  border: var(--hairline-strong);
}

.btn-secondary:hover:not(:disabled) {
  background: var(--surface-ground);
}

/* --------------------------------------------------------------------------
   Sacred-path failure state (auth) — named problem, plain Arabic, no icon
   dingbat, carried by weight + color together (DESIGN.md §1: never hue alone).
   -------------------------------------------------------------------------- */

.form-error {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  color: var(--signal-danger);
  font-weight: var(--weight-bold);
  line-height: var(--leading-control);
}

.form-error svg {
  flex-shrink: 0;
  margin-block-start: 0.15em;
}

.form-error[hidden] { display: none; }

/* Passive "a newer copy exists" note (tenant screen, cache-first — the
   2026-08-05 panel-network-resilience plan). Informational, not a danger
   state: it must not shout over a form mid-edit. */
.tenant-fresh-note {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-2);
  background: var(--surface-raised);
  border: 1px solid var(--basalt-hairline);
  border-radius: var(--radius-surface);
  font-weight: var(--weight-bold);
}

/* Quiet success feedback (settings screen's change-password block, Phase 5
   Task 5) — same shape as .form-error, zinjar instead of fuwwa: a completed
   sacred-path action is a "live" signal, not a decorative green (DESIGN.md
   §1's no-generic-SaaS-green rule applies here too). */
.form-success {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  color: var(--signal-live);
  font-weight: var(--weight-bold);
  line-height: var(--leading-control);
}

.form-success svg {
  flex-shrink: 0;
  margin-block-start: 0.15em;
}

.form-success[hidden] { display: none; }

/* --------------------------------------------------------------------------
   Forgot-password line — no reset flow (spec §4); points to a human.
   -------------------------------------------------------------------------- */

.auth-help {
  margin-block-start: var(--space-5);
  text-align: center;
  color: var(--ink-soft);
}

.auth-help a {
  color: var(--ink);
  font-weight: var(--weight-bold);
  text-decoration-thickness: from-font;
}

/* --------------------------------------------------------------------------
   Signed-in minimal shell (index.html, phase 1) — the real home is phase 2.
   -------------------------------------------------------------------------- */

.signedin-card {
  background: var(--surface-raised);
  border: var(--hairline);
  border-radius: var(--radius-surface);
  padding: var(--space-6) var(--space-5);
  text-align: center;
}

.signedin-greeting {
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--text-title);
  line-height: var(--leading-title);
  word-break: break-word;
}

.signedin-role {
  margin-block-start: var(--space-2);
  color: var(--ink-soft);
}

.signedin-card .btn {
  margin-block-start: var(--space-6);
}

/* --------------------------------------------------------------------------
   Skeleton — loading state while the auth guard resolves. Static blocks by
   default; the gentle pulse conveys "still working" (not decoration) and is
   the only motion on this screen, so it never competes with the publish bar
   signature (DESIGN.md §5 reserves continuous motion to the course).
   -------------------------------------------------------------------------- */

.skeleton-line {
  height: 1.25rem;
  border-radius: var(--radius-control);
  background: var(--basalt-hairline);
  animation: skeleton-pulse 1400ms var(--ease-out) infinite;
}

.skeleton-line + .skeleton-line {
  margin-block-start: var(--space-3);
}

.skeleton-line--title {
  height: 1.75rem;
  width: 70%;
  margin-inline: auto;
}

.skeleton-line--wide { width: 100%; }
.skeleton-line--narrow { width: 45%; margin-inline: auto; }

.skeleton-line--btn {
  height: var(--tap-min);
  margin-block-start: var(--space-6);
}

@keyframes skeleton-pulse {
  0%, 100% { opacity: 0.55; }
  50% { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .skeleton-line,
  .chrome-header-name-skeleton { animation: none; opacity: 0.75; }
}

/* --------------------------------------------------------------------------
   Save-in-flight state (form-busy.js — owner-feedback round A, item 4).
   The first test user edited two more fields into a pending save and read the
   result as lost work: the button said «جارِ الحفظ…» but nothing else moved
   and every field stayed live.
   -------------------------------------------------------------------------- */

/* A ring that turns, beside the button's own busy label. `currentColor` so it
   inherits the button's text colour on primary and secondary alike, with no
   second rule. */
.btn-spinner {
  display: inline-block;
  inline-size: 1em;
  block-size: 1em;
  margin-inline-end: var(--space-2);
  vertical-align: -0.125em;
  border: 2px solid currentColor;
  border-block-start-color: transparent;
  border-radius: 50%;
  animation: btn-spin 700ms linear infinite;
}

/* The fieldset exists only to carry `disabled` — it must be visually
   invisible, or every form on every screen gains a box it never had. */
.form-busy-fieldset {
  border: 0;
  margin: 0;
  padding: 0;
  min-inline-size: 0; /* the UA default breaks flex/grid children */
}

/* Deliberately no opacity/greyscale on the disabled state: the fields are
   inert for about a second, and dimming a whole screen for that reads as an
   error rather than as progress. The cursor and the spinner carry it. */
.form-busy-fieldset[disabled] {
  cursor: progress;
}

@keyframes btn-spin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  /* Keep the ring as a static mark rather than hiding it — it is the only
     persistent visual that the save is still running. Same stance the
     skeleton-pulse block above takes. */
  .btn-spinner { animation: none; opacity: 0.75; }
}

/* --------------------------------------------------------------------------
   Shared chrome — header + the fixed bottom dock (nav + publish bar).
   Only present on screens that call mountChrome() (chrome.js); login.html
   never carries body.has-chrome, so .shell there is untouched.
   -------------------------------------------------------------------------- */

body.has-chrome {
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
}

body.has-chrome .shell {
  flex: 1;
  min-height: 0;
  justify-content: flex-start;
  padding-block-end: calc(var(--chrome-footer-height, 0px) + var(--space-6));
}

.chrome-header {
  background: var(--surface-raised);
  border-block-end: var(--hairline);
}

/* --space-2 block padding, not --space-4: the sign-out control is already a
   full --tap-min (48px) box, so --space-4 made every screen's header 80px —
   ~10% of a 390x844 phone, permanently, mostly to frame a once-a-month
   action. At --space-2 the header is 64px and the 48px tap target is
   untouched. */
.chrome-header-inner {
  max-width: var(--spine-outer);
  margin-inline: auto;
  padding: var(--space-2) var(--space-4);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.chrome-header-identity {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.chrome-header-name {
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--text-title);
  line-height: var(--leading-title);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Pre-data chrome mount (chrome.js buildHeader with businessName null):
   sits INSIDE the name <p> so the reserved line height is identical before
   and after setSite fills the real name — no header shift. */
.chrome-header-name-skeleton {
  display: inline-block;
  inline-size: 7ch;
  block-size: 0.7em;
  vertical-align: middle;
  border-radius: var(--radius-control);
  background: var(--basalt-hairline);
  animation: skeleton-pulse 1400ms var(--ease-out) infinite;
}

/* The agency site-switcher's context line (Task 4) — only rendered when an
   agency account is inside an owner panel, under the (someone else's)
   business name above it, so it never looks like part of that name. */
.chrome-header-agency-back {
  align-self: flex-start;
  margin-inline-start: calc(-1 * var(--space-2));
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding-inline: var(--space-2);
  color: var(--ink-soft);
  font-weight: var(--weight-bold);
  text-decoration: underline;
  text-decoration-thickness: from-font;
}

.chrome-header-agency-back:hover {
  color: var(--ink);
}

/* Quiet text control, not a bordered button (chrome.js's own comment on why).
   The negative inline margin pulls the padding back out to the header edge so
   the label optically lines up with the header's own inline padding rather
   than sitting --space-3 short of it. */
.chrome-header-signout {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  margin-inline-end: calc(-1 * var(--space-3));
  padding-inline: var(--space-3);
  background: none;
  border: none;
  border-radius: var(--radius-control);
  color: var(--ink-soft);
  font-size: var(--text-body);
  font-weight: var(--weight-bold);
  line-height: var(--leading-control);
  cursor: pointer;
  transition: color var(--motion-quick) var(--ease-out),
              background-color var(--motion-quick) var(--ease-out);
}

.chrome-header-signout:hover:not(:disabled) {
  color: var(--ink);
  background: var(--surface-ground);
}

.chrome-header-signout:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

/* Owner passive suspended indicator (Phase 7 Task 3, cms/CLAUDE.md backlog:
   "Owner panel has no passive suspended indicator") — a second row inside
   the header, present on every owner screen mountChrome guards, shown only
   while site.status.suspended is true (chrome.js's updateSuspendedBanner).
   No dismiss and no close control: it reflects live state, not a message
   the owner can act on locally — the only way it goes away is the agency's
   own تفعيل الموقع (task-3-brief.md). Reuses the amber-caution pairing this
   app already carries OUTSIDE the publish bar itself for a light-surface
   "heads up" cue (.rotation-warning, .tenant-suspend-skipped-note) —
   kahraman-tint ground, kahraman-deep text, ~5.3:1 verified elsewhere in
   this exact codebase — rather than the publish bar's own reserved
   "unpublished changes" meaning for plain kahraman. Living inside `header`
   (not a separate fixed element) means chrome.js's existing
   applyChromeMetrics() — which already measures header.offsetHeight on
   every render() — picks up the extra row's height for free; no new CSS
   variable needed. */
.chrome-suspended-banner {
  background: var(--kahraman-tint);
  border-block-end: var(--hairline);
}

.chrome-suspended-banner[hidden] { display: none; }

.chrome-suspended-banner-inner {
  max-width: var(--spine-outer);
  margin-inline: auto;
  padding: var(--space-3) var(--space-4);
  color: var(--kahraman-deep);
  font-weight: var(--weight-bold);
}

/* Fixed dock: publish bar on top, tab nav as the true block-end edge — the
   bar's own safe-area clearance rides on the nav below it. Desktop
   (min-width: 64rem, below) unwraps this into two independently-fixed
   pieces: the rail and the bar. */
.chrome-footer {
  position: fixed;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: var(--layer-bar);
  display: flex;
  flex-direction: column;
  padding-block-end: var(--safe-block-end);
}

.chrome-nav {
  background: var(--surface-raised);
  border-block-start: var(--hairline);
}

/* Full-bleed backdrop, centered content — the same spine as
   .chrome-header-inner (DESIGN.md §3). Mobile-and-up default; the desktop
   breakpoint below turns this into the inline-start rail instead. */
.chrome-nav-inner {
  display: flex;
  max-width: var(--spine-outer);
  margin-inline: auto;
}

.chrome-nav-tab {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  padding-block: var(--space-2);
  border-block-start: 2px solid transparent;
  color: var(--ink-soft);
  font-weight: var(--weight-bold);
  text-decoration: none;
  text-align: center;
}

.chrome-nav-tab:hover { color: var(--ink); }

.chrome-nav-tab[aria-current="page"] {
  border-block-start-color: var(--basalt);
  color: var(--ink);
}

/* ---- Desktop: the nav becomes a modest inline-start rail (DESIGN.md §3) --
   The header stays a full-width top bar (a rail below a top bar is the
   standard, well-understood shape); the rail starts under it and runs to
   the block-end edge. .chrome-footer un-boxes via display:contents so the
   bar and nav can each set their own position:fixed independently — the
   nav is no longer stacked under the bar, so the bar spans the remaining
   inline space instead. chrome.js keeps --chrome-header-height/
   --chrome-footer-height in sync with this breakpoint at runtime. */
@media (min-width: 64rem) {
  :root {
    --chrome-rail-width: 6rem;
  }

  /* rail + the shell's own inline padding, NOT the rail alone. The chrome
     pieces clear the rail on their outer box and then add --space-4 inside
     their centred inner; the shell has to do the same or its centred inner
     sits 8px off theirs — which is exactly the residual misalignment left
     after the rail offset was added to the header. With this, the header
     name, the page title, and the publish-bar headline all land on one
     spine at every width. */
  body.has-chrome .shell {
    padding-inline-start: calc(var(--chrome-rail-width) + var(--space-4));
  }

  /* The header spans the full width (it sits ABOVE the rail), but its inner
     was centering on that full width while the content column centers inside
     the rail-offset area — so at 1280px the header title and the page title
     below it sat ~300px apart on two different spines. The rail offset has to
     be on the header too. .publish-bar-row already gets this for free (the
     bar is inset-inline: rail 0), which is why the bar was the only piece
     already lined up with the content. */
  .chrome-header,
  .chrome-suspended-banner {
    padding-inline-start: var(--chrome-rail-width);
  }

  .chrome-footer {
    display: contents;
  }

  .chrome-nav {
    position: fixed;
    inset-inline-start: 0;
    inset-block: var(--chrome-header-height, 0px) 0;
    width: var(--chrome-rail-width);
    z-index: var(--layer-bar);
    display: flex;
    border-block-start: none;
    border-inline-end: var(--hairline);
  }

  .chrome-nav-inner {
    flex-direction: column;
    max-width: none;
    margin-inline: 0;
    width: 100%;
    padding-block-start: var(--space-6);
  }

  /* The active marker rides the rail's CONTENT-facing edge, not its outer
     one. On the outer edge it landed flush against the viewport boundary in
     RTL (x=1279 at 1280px) — a 3px basalt stub hanging at the very edge of
     the screen, which reads as a rendering artifact rather than "you are
     here". On the inner edge it points at the content it selects, and the
     selected tab also takes the ground tint so the state survives without
     depending on a 3px line at all. */
  .chrome-nav-tab {
    flex: none;
    border-block-start: none;
    border-inline-end: 3px solid transparent;
    padding-block: var(--space-4);
  }

  .chrome-nav-tab[aria-current="page"] {
    border-block-start-color: transparent;
    border-inline-end-color: var(--basalt);
    background: var(--surface-ground);
  }

  .publish-bar {
    position: fixed;
    inset-inline: var(--chrome-rail-width) 0;
    inset-block-end: 0;
    z-index: var(--layer-bar);
  }
}

/* ==========================================================================
   The publish bar — السطر / the course (DESIGN.md §4, the signature).
   Every state = bg + ink + course pattern + copy, never hue alone. Course
   direction below is deliberately physical (not logical): the "to left"
   masonry-laying metaphor in DESIGN.md §4.3 is a fixed real-world direction,
   not page flow — it does not mirror under RTL.
   ========================================================================== */

/* The pre-data chrome mounts the bar with the `hidden` attribute (chrome.js
   — no truthful state to show until setSite). The display:flex right below
   would silently beat the UA's [hidden]{display:none} — an author rule wins
   over the UA sheet regardless of specificity — leaving an empty white slab
   docked at the bottom, so hidden needs its own author rule. */
.publish-bar[hidden] { display: none; }

.publish-bar {
  display: flex;
  flex-direction: column;
  min-height: var(--bar-min-height);
  box-shadow: var(--shadow-bar);
  background: var(--publish-saved-bg);
  color: var(--publish-saved-ink);
  transition: background-color var(--motion-settle) var(--ease-out),
              color var(--motion-settle) var(--ease-out);
}

.publish-bar[data-state="saved"] {
  background: var(--publish-saved-bg);
  color: var(--publish-saved-ink);
  --meta-ink: var(--publish-saved-meta);
}

.publish-bar[data-state="pending"] {
  background: var(--publish-pending-bg);
  color: var(--publish-pending-ink);
  --meta-ink: var(--publish-pending-meta);
  --action-bg: var(--publish-pending-action-bg);
  --action-ink: var(--publish-pending-action-ink);
}

.publish-bar[data-state="busy"] {
  background: var(--publish-busy-bg);
  color: var(--publish-busy-ink);
  --meta-ink: var(--publish-busy-meta);
  --action-bg: var(--publish-pending-action-bg);
  --action-ink: var(--publish-pending-action-ink);
}

.publish-bar[data-state="published"] {
  background: var(--publish-live-bg);
  color: var(--publish-live-ink);
  --meta-ink: var(--publish-live-meta);
}

.publish-bar[data-state="failed"] {
  background: var(--publish-failed-bg);
  color: var(--publish-failed-ink);
  --meta-ink: var(--publish-failed-meta);
  --action-bg: var(--publish-failed-action-bg);
  --action-ink: var(--publish-failed-action-ink);
}

/* saved-draft: a CONTINUOUS course — every stone laid, no gaps. DESIGN.md §4
   specifies exactly that ("all published; continuous course"), but the rule
   shipped as an 18px-on / 4px-off repeat, i.e. still a dashed band. That cost
   the signature its whole point: the difference between "everything is
   published" and "changes are waiting" was two dash lengths (18/22 vs 8/22)
   rather than laid-stone vs. gaps, which is not a distinction anyone reads
   at a glance on a phone. Solid here; every other state keeps its own
   pattern, so the gap now means something. */
.publish-bar-course {
  height: var(--course-size);
  flex-shrink: 0;
  background-repeat: repeat-x;
  background-position: 0 0;
  background-size: 22px var(--course-size);
  background-image: linear-gradient(
    var(--publish-saved-course), var(--publish-saved-course)
  );
}

.publish-bar[data-state="pending"] .publish-bar-course {
  background-image: repeating-linear-gradient(
    to left, var(--publish-pending-course) 0 8px, transparent 8px 22px
  );
}

.publish-bar[data-state="busy"] .publish-bar-course {
  background-image: repeating-linear-gradient(
    to left, var(--publish-busy-course) 0 12px, transparent 12px 22px
  );
  animation: course-fill var(--motion-course) linear infinite;
}

/* Continuous, like saved — this state IS "the course is finished", and it
   settles into saved a few seconds later. A dashed band here would have said
   the opposite of the confirmation it carries. */
.publish-bar[data-state="published"] .publish-bar-course {
  background-image: linear-gradient(
    var(--publish-live-course), var(--publish-live-course)
  );
  animation: course-settle var(--motion-settle) var(--ease-out);
}

/* Also continuous — the "stopped short" reading is the mask below, a wall
   that ends. Built on a dashed band the mask read as one more gap among
   many instead of the one that matters. */
.publish-bar[data-state="failed"] .publish-bar-course {
  background-image: linear-gradient(
    var(--publish-failed-course), var(--publish-failed-course)
  );
  /* the wall stopped short — mask out the final stretch of the course */
  mask-image: linear-gradient(to left, black 0 82%, transparent 82% 100%);
  -webkit-mask-image: linear-gradient(to left, black 0 82%, transparent 82% 100%);
}

@keyframes course-fill {
  from { background-position-x: 0; }
  to { background-position-x: -22px; }
}

@keyframes course-settle {
  from { opacity: 0; }
  to { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .publish-bar[data-state="busy"] .publish-bar-course { animation: none; }
  .publish-bar[data-state="published"] .publish-bar-course { animation: none; }
}

.publish-bar-row {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  /* --space-4 inline, matching every other spine holder — at --space-5 the
     bar's headline sat 8px inboard of the content column it reports on. */
  padding: var(--space-3) var(--space-4);
  max-width: var(--spine-outer);
  margin-inline: auto;
  box-sizing: border-box;
  width: 100%;
}

.publish-bar-text {
  min-width: 0;
}

.publish-bar-headline {
  font-weight: var(--weight-bold);
  font-size: var(--text-emphasis);
  line-height: var(--leading-control);
}

.publish-bar-meta {
  margin-block-start: var(--space-1);
  color: var(--meta-ink);
  font-size: var(--text-body);
  line-height: var(--leading-control);
}

/* Revert durability's skipped-images note (Phase 7 Task 2, chrome.js's
   buildBarRow "published" branch) — only ever appears inside data-state
   ="published" (zinjar ground), styled with the note-specific ink token
   (tokens.css) rather than --meta-ink, which the pending/failed states
   repurpose for their own retry copy. */
.publish-bar-skipped-note {
  margin-block-start: var(--space-1);
  color: var(--publish-live-note-ink);
  font-size: var(--text-body);
  line-height: var(--leading-control);
}

/* invalid-data's raw validator lines (chrome.js buildBarRow, failed state
   only — audit P5). Latin technical strings by convention, so the list runs
   LTR while staying flush against the bar's RTL text edge; --meta-ink is
   already the failed state's own subordinate ink (the [data-state="failed"]
   scope above redefines it), so the lines sit under the Arabic reason at
   the meta line's exact weight, never competing with it. */
.publish-bar-error-details {
  margin-block-start: var(--space-1);
  padding: 0;
  list-style: none;
  color: var(--meta-ink);
  font-size: var(--text-body);
  line-height: var(--leading-control);
  direction: ltr;
  text-align: right;
  overflow-wrap: anywhere;
}

.ledger-numerals {
  font-feature-settings: var(--font-features-price);
  font-variant-numeric: tabular-nums lining-nums;
}

/* Quiet by default (§1: color is state, never decoration on a control) — the
   link inherits the bar's own ink. State 4 doubles it as the meta line
   itself (DESIGN.md §4.4: "tinted from the zinjar hue"), so that combination
   overrides back to --meta-ink via the higher-specificity rule below.
   inline-flex + min-height (not padding alone) is what actually clears
   --tap-min: state 1's copy calls this an "Action" (§4.1), so its hit area
   holds the same 48px floor as the نشر/جرّب buttons beside it — padding-only
   at --space-2 undershot that (P2 final review finding). */
.publish-bar-link {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  min-height: var(--tap-min);
  color: inherit;
  font-weight: var(--weight-bold);
  text-decoration: underline;
  text-decoration-thickness: from-font;
  padding-inline: var(--space-2);
  margin-inline: calc(var(--space-2) * -1);
}

.publish-bar-meta.publish-bar-link {
  color: var(--meta-ink);
}

.publish-bar-action {
  flex-shrink: 0;
  min-height: var(--tap-min);
  padding-inline: var(--space-5);
  border: none;
  border-radius: var(--radius-control);
  background: var(--action-bg, var(--basalt));
  color: var(--action-ink, var(--ink-on-inverse));
  font-size: var(--text-body);
  font-weight: var(--weight-bold);
  line-height: var(--leading-control);
  cursor: pointer;
  transition: background-color var(--motion-quick) var(--ease-out),
              opacity var(--motion-quick) var(--ease-out);
}

.publish-bar-action:hover:not(:disabled) {
  background: color-mix(in srgb, var(--action-bg, var(--basalt)) 88%, black);
}

.publish-bar-action:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

/* Dark bar states need the bright ring, not the deep one (DESIGN.md §6). */
.publish-bar[data-state="busy"] :focus-visible,
.publish-bar[data-state="published"] :focus-visible,
.publish-bar[data-state="failed"] :focus-visible {
  outline: var(--focus-ring-on-dark);
}

/* ==========================================================================
   Home hub (index.html, screen 3 — DESIGN.md §3 / cms.md §3). One level
   deep, everything reachable from here: a status record echoing the bar's
   own state, and the 3 highest-frequency tasks as full-bleed rows, not
   cards (DESIGN.md §3: rows are the workbench surface).
   ========================================================================== */

/* The chrome's header/nav/bar all run to --content-measure; the phase-1
   shell was narrower (a login-card measure) — widen it once chrome is
   mounted so real content shares the same spine instead of sitting in a
   column visibly narrower than the bar above it. login.html has no
   .has-chrome body class, so its auth card keeps the tighter measure. */
body.has-chrome .shell-inner {
  max-width: var(--content-measure);
}

.home {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}

/* The live-site card (index.html's own comment covers what it is now, and
   what it deliberately stopped restating). Reduced to a last-published line
   and the link, it no longer needs the pending/live tone tint, the headline
   type step, or the background transition those drove — a single quiet
   raised strip is the whole component. */
.home-status {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  background: var(--surface-raised);
  border: var(--hairline);
  border-radius: var(--radius-surface);
  padding: var(--space-3) var(--space-4);
}

.home-status-meta {
  color: var(--ink-soft);
}

.home-status-link {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  color: var(--ink);
  font-weight: var(--weight-bold);
  text-decoration: underline;
  text-decoration-thickness: from-font;
}

.home-section-title {
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--text-title);
  line-height: var(--leading-title);
}

.home-task-list {
  margin-block-start: var(--space-3);
  border-block-start: var(--hairline);
}

.home-task-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: var(--row-min-height);
  padding-inline: var(--space-2);
  border-block-end: var(--hairline);
  color: var(--ink);
  font-weight: var(--weight-bold);
  text-decoration: none;
}

.home-task-row:hover {
  background: var(--surface-raised);
}

.home-task-chevron {
  flex-shrink: 0;
  color: var(--ink-soft);
}

/* The revert escape hatch (cms.md §2, Task 7) — deliberately quieter than
   the 3 task rows above it: ink-soft, no bold weight, its own section so it
   doesn't read as a 4th "most-used job". Only ever shown once something has
   actually published (hasLiveSite gate in index.html — nothing to revert to
   before then). */
.home-revert-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  width: 100%;
  min-height: var(--tap-min);
  padding-inline: var(--space-2);
  background: none;
  border: none;
  color: var(--ink-soft);
  font-size: var(--text-body);
  text-align: start;
  cursor: pointer;
}

.home-revert-trigger:hover {
  color: var(--ink);
}

/* ---- revert sheet — reuses .sheet/.sheet-backdrop/.sheet-action (page-items.js) ---- */

.revert-list-status {
  padding: var(--space-4) var(--space-2);
  color: var(--ink-soft);
}

.revert-version {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-1);
  padding-block: var(--space-3);
}

.revert-version-by {
  font-weight: var(--weight-regular);
  color: var(--ink-soft);
}

/* ==========================================================================
   Items screen (items.html, screen 4 — cms.md §3 / DESIGN.md §3). Rows, not
   cards: full-bleed hairline-separated rows, price editable in place. Every
   control here is a *tool*, so it stays basalt/neutral — the amber/verdigris
   /madder signal triad (DESIGN.md §1) is reserved for publish state and
   destructive actions, never borrowed for generic on/off UI.
   ========================================================================== */

.items-screen,
.categories-screen,
.list-editor-screen,
.console-screen {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.page-title {
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--text-title);
  line-height: var(--leading-title);
}

/* ---- Console shell (console.html, Phase 6 Task 1) — title and the single
   create action share one row. ---- */

.console-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
}

/* Overrides .btn-primary's width:100% — this action is two words, and a
   full-measure slab under the title outweighed the list it sits above. */
.console-add {
  width: auto;
  padding-inline: var(--space-6);
}

/* ---- Tenants list (console.html, Phase 6 Task 2) — rows, not cards, the
   same workbench convention as items/categories (DESIGN.md §3): full-bleed
   hairline rows, the whole row a single tap target (like page-site.js's
   .site-section-row) since there are no per-row controls here, only
   navigation into the tenant-detail record (Task 3). Search below reuses
   .items-search/-input verbatim — those names stay generic on purpose (see
   page-items.js) so a second list screen doesn't need its own copy.
   Every chip/badge here stays at the inherited 1rem body size (DESIGN.md
   §6: "nothing in the panel renders smaller") — sized by padding alone,
   the same restraint .item-badge already uses. ---- */

/* Orientation line under the screen title — how many clients are in view and
   how many the surfacing sort flagged. Sits with the title, not with the
   list, so it reads as a subtitle rather than as a row. */
.console-summary {
  margin-block-end: var(--space-4);
  color: var(--ink-soft);
}

.tenant-pinned-section,
.tenant-rest-section {
  display: flex;
  flex-direction: column;
}

.tenant-rest-section {
  margin-block-start: var(--space-5);
}

.tenant-pinned-heading,
.tenant-rest-heading {
  padding-inline: var(--space-1);
  padding-block-end: var(--space-2);
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--text-emphasis);
}

.tenant-pinned-heading {
  color: var(--kahraman-deep);
}

/* The unflagged half of the book — a plain ink-soft heading, not a second
   amber one. It is a boundary marker, not a signal. */
.tenant-rest-heading {
  color: var(--ink-soft);
}

.tenant-list {
  border-block-start: var(--hairline);
}

/* A heavier top rule than the plain hairline — the pinned block is a
   distinct, more urgent zone, not just another list (course-band echo,
   DESIGN.md §4, without borrowing the bar's own animated pattern). */
.tenant-list--pinned {
  border-block-start: 2px solid var(--kahraman-deep);
}

.tenant-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--row-min-height);
  padding-block: var(--space-3);
  padding-inline: var(--space-1);
  border-block-end: var(--hairline);
  color: inherit;
  text-decoration: none;
}

.tenant-row:hover {
  background: var(--surface-raised);
}

.tenant-row-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.tenant-row-top {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.tenant-row-name {
  font-weight: var(--weight-bold);
  word-break: break-word;
}

/* Line two: the row's values as discrete facts, divided by a hairline rather
   than by a "·" character. page-console.js's own comment covers why the dot
   had to go (it reads as ٠ against Arabic-Indic digits). border-inline-start
   in RTL is the fact's right edge — i.e. the side facing the fact before it —
   so the rule lands between them and never before the first one. */
.tenant-row-facts {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--space-3);
  color: var(--ink-soft);
}

.tenant-fact {
  white-space: nowrap;
}

.tenant-fact + .tenant-fact {
  padding-inline-start: var(--space-3);
  border-inline-start: var(--hairline);
}

/* Status pill — active/overdue/suspended/churned, the exact signal mapping
   the phase-6 plan pins (Global Constraints): live work is zinjar, overdue
   is the amber "waiting" signal, suspended is fuwwa (trouble), churned is
   the quiet neutral (no signal spent on a relationship that's already
   over — see DESIGN.md §1's "color is state, never decoration" applied to
   the one status that ISN'T actionable). */
.tenant-status {
  flex-shrink: 0;
  padding-inline: var(--space-2);
  padding-block: 0.1em;
  border-radius: var(--radius-control);
  font-weight: var(--weight-bold);
}

/* Outline, not a zinjar fill. "نشط" is the default state of almost every row
   in the book, so filling it made six of nine rows carry a solid signal block
   — and the two pills that actually mean "do something" (overdue amber,
   suspended fuwwa) had to shout over them. The quiet treatment applies
   DESIGN.md §1's own logic ("color is state, never decoration") the same way
   --churned already does: the non-actionable state does not spend a fill.
   Zinjar text on jass measures 5.9:1, on the hajar ground 5.1:1. */
.tenant-status--active {
  background: transparent;
  color: var(--zinjar);
  border: 1px solid color-mix(in srgb, var(--zinjar) 40%, transparent);
}

.tenant-status--overdue {
  background: var(--kahraman-tint);
  color: var(--kahraman-deep);
}

.tenant-status--suspended {
  background: var(--fuwwa);
  color: var(--ink-on-signal);
}

.tenant-status--churned {
  background: var(--hajar);
  color: var(--ink-soft);
  border: var(--hairline-strong);
}

/* Two-tier visual weight on purpose — domains outrank fees (D9), so the
   domain-expiring badge is a solid kahraman-deep fill (the stronger mark)
   and the overdue badge is the softer kahraman-tint/kahraman-deep pairing
   the publish bar's own pending state already uses, so the same "waiting,
   not yet trouble" read carries over from the bar the agency already knows. */
.tenant-reason {
  padding-inline: var(--space-2);
  padding-block: 0.1em;
  border-radius: var(--radius-control);
  font-weight: var(--weight-bold);
}

.tenant-reason--domain-expiring {
  background: var(--kahraman-deep);
  color: var(--ink-on-inverse);
}

.tenant-reason--overdue {
  background: var(--kahraman-tint);
  color: var(--kahraman-deep);
}

/* Phase 7 Task 4 (cms/CLAUDE.md backlog: "Missing-nextDueDate tenants never
   pin") — deliberately the QUIETEST of the three reason badges, one tier
   below even .tenant-reason--overdue's kahraman-tint: this isn't a
   billing/domain alarm, it's "no date on file yet." Reuses
   .tenant-status--churned's own neutral hajar/ink-soft/hairline pairing
   verbatim rather than inventing a fourth badge treatment. */
.tenant-reason--missing-next-due-date {
  background: var(--hajar);
  color: var(--ink-soft);
  border: var(--hairline-strong);
}

.tenant-row-chevron {
  flex-shrink: 0;
  color: var(--ink-soft);
}

/* ---- Tenant detail (tenant.html, Phase 6 Task 3) — the header repeats the
   list row's own city/tier/status vocabulary (.tenant-row-facts/.tenant-fact
   and .tenant-status, reused verbatim) so a tenant looks like the same record
   whether it's a row in the list or the page you landed on from tapping it.
   The record editor below is plain .item-form/.item-fields (the exact shell
   every owner-facing section-form screen already uses); quick actions reuse
   .settings-block + .field-group--row (switch fields' own row shape) for a
   label/hint pair with a trailing control, needing only one small override:
   .btn defaults to width:100% for .btn-primary, which would otherwise blow
   out this row's own layout. ---- */

.tenant-detail-header {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.tenant-detail-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  color: var(--ink-soft);
}

.field-group--row .btn {
  width: auto;
}

/* Suspend/unsuspend confirm sheet (Phase 6 Task 5) — reuses the reset-
   password sheet's exact shell (.sheet-backdrop/.sheet--scrollable/
   .sheet-header/.sheet-body), just with two side-by-side buttons instead
   of one full-width submit — a confirm needs an equally-weighted way out,
   not just a corner ✕. */
.sheet-confirm-actions {
  display: flex;
  gap: var(--space-3);
  margin-block-start: var(--space-3);
}

.sheet-confirm-actions .btn {
  flex: 1;
  width: auto;
}

.items-toolbar {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.items-add-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
}

.items-search {
  position: relative;
  display: flex;
  align-items: center;
}

.items-search-icon {
  position: absolute;
  inset-inline-start: var(--space-3);
  color: var(--ink-soft);
  pointer-events: none;
}

.items-search-input {
  width: 100%;
  min-height: var(--control-height);
  padding-inline-start: var(--space-7);
  padding-inline-end: var(--space-4);
  background: var(--surface-raised);
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  font-size: var(--text-body);
}

.items-search-input:focus-visible {
  border-color: var(--kahraman-deep);
}

.items-chips {
  display: flex;
  gap: var(--space-2);
  overflow-x: auto;
  padding-block: var(--space-1);
  scrollbar-width: none;
}

.items-chips::-webkit-scrollbar {
  display: none;
}

.items-chip {
  flex-shrink: 0;
  min-height: var(--tap-min);
  padding-inline: var(--space-4);
  display: flex;
  align-items: center;
  background: var(--surface-raised);
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  font-weight: var(--weight-bold);
  color: var(--ink-soft);
  cursor: pointer;
}

.items-chip[aria-pressed="true"] {
  background: var(--basalt);
  border-color: var(--basalt);
  color: var(--ink-on-inverse);
}

.items-list-container {
  min-width: 0;
}

.items-list {
  border-block-start: var(--hairline);
}

.items-category-heading {
  margin-block-start: var(--space-5);
  padding-inline: var(--space-1);
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--text-emphasis);
  color: var(--ink-soft);
}

.items-category-heading:first-child {
  margin-block-start: var(--space-3);
}

.items-no-results {
  padding-block: var(--space-6);
  text-align: center;
  color: var(--ink-soft);
  font-weight: var(--weight-bold);
}

/* ---- item row ---- */

.item-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding-block: var(--space-3);
  padding-inline: var(--space-1);
  border-block-end: var(--hairline);
}

.item-row[data-visible="false"] {
  opacity: 0.55;
}

.item-row-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
}

/* Leading thumbnail (Task 3) — fixed size so a late-loading image never
   reshuffles the row (no CLS), object-fit cover so any source aspect ratio
   still fills the square. Absent entirely for image-less items — nothing
   reserves its space, so those rows are unaffected. */
.item-row-thumb {
  flex-shrink: 0;
  display: block;
  width: 2.75rem;
  height: 2.75rem;
  object-fit: cover;
  background: var(--hajar);
  border: var(--hairline);
  border-radius: var(--radius-control);
}

.item-row-identity {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.item-row-name {
  font-weight: var(--weight-bold);
  word-break: break-word;
  user-select: none; /* long-press toggles the badge — a text-selection popover fighting it reads as broken */
}

.item-badge {
  flex-shrink: 0;
  padding-inline: var(--space-2);
  padding-block: 0.1em;
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  font-weight: var(--weight-bold);
  color: var(--ink-soft);
}

.item-row-controls {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

.item-visibility {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.item-visibility-label {
  color: var(--ink-soft);
  font-weight: var(--weight-bold);
}

/* Visibility switch — the button IS the ≥48px tap target (quality floor,
   DESIGN.md §6), sized to --tap-min in both dimensions; the visible 48×28
   pill is an inner track centered inside it, so the small switch doesn't
   shrink the hit area for the second-most-tapped control on the row. The
   pill shape stays exactly where DESIGN.md §3 reserves it ("pill only for
   toggle knobs") — basalt when on (no signal color spent on a generic
   switch), hajar/hairline when off. */
.item-switch {
  flex-shrink: 0;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  border-radius: var(--radius-control);
  padding: 0;
  cursor: pointer;
}

.item-switch-track {
  width: 3rem;
  height: 1.75rem;
  padding: 0.2rem;
  border-radius: var(--radius-pill);
  border: var(--hairline-strong);
  background: var(--hajar);
  display: flex;
  align-items: center;
  justify-content: start;
  transition: background-color var(--motion-quick) var(--ease-out),
              border-color var(--motion-quick) var(--ease-out);
}

.item-switch[aria-checked="true"] .item-switch-track {
  background: var(--basalt);
  border-color: var(--basalt);
  justify-content: end;
}

.item-switch-knob {
  width: 1.25rem;
  height: 1.25rem;
  border-radius: var(--radius-pill);
  background: var(--surface-raised);
  box-shadow: var(--shadow-knob);
}

.item-overflow-btn,
.item-reorder-btn,
.item-price-confirm,
.item-price-cancel,
.category-rename-btn,
.category-delete-btn,
.category-rename-confirm,
.category-rename-cancel,
.list-editor-edit-btn,
.list-editor-delete-btn {
  flex-shrink: 0;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  color: var(--ink);
  cursor: pointer;
}

.item-overflow-btn:hover,
.item-reorder-btn:hover:not(:disabled),
.item-price-confirm:hover:not(:disabled),
.item-price-cancel:hover:not(:disabled),
.category-rename-btn:hover,
.category-delete-btn:hover,
.category-rename-confirm:hover:not(:disabled),
.category-rename-cancel:hover:not(:disabled),
.list-editor-edit-btn:hover,
.list-editor-delete-btn:hover {
  background: var(--hajar);
}

.item-reorder-btn:disabled,
.item-price-confirm:disabled,
.item-price-cancel:disabled,
.category-rename-confirm:disabled,
.category-rename-cancel:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* Delete is destructive (DESIGN.md §1: fuwwa is failure/destructive-only) —
   same square icon-button chrome as its neighbours, tinted so it never
   looks identical to the quiet rename/reorder controls beside it. */
.category-delete-btn,
.list-editor-delete-btn {
  color: var(--signal-danger);
}

.item-row-bottom {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.item-price-region {
  display: flex;
  align-items: center;
  min-height: var(--tap-min);
  min-width: 0;
}

.item-price {
  background: none;
  border: none;
  margin-inline-start: calc(var(--space-2) * -1);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-control);
  font-weight: var(--weight-bold);
  font-size: var(--text-price);
  color: var(--ink);
  cursor: pointer;
}

.item-price:hover {
  background: var(--hajar);
}

.item-price-static {
  font-weight: var(--weight-bold);
  font-size: var(--text-price);
  color: var(--ink-soft);
}

.item-price-edit,
.category-rename-edit,
.categories-add-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.item-price-input,
.category-rename-input {
  min-height: var(--tap-min);
  padding-inline: var(--space-2);
  font-size: var(--text-body);
  font-weight: var(--weight-bold);
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  background: var(--surface-raised);
}

.item-price-input { width: 7rem; }
.category-rename-input { flex: 1; min-width: 10rem; }

.item-price-input:focus-visible,
.category-rename-input:focus-visible {
  border-color: var(--kahraman-deep);
}

.item-price-error,
.item-row-error,
.category-rename-error {
  flex-basis: 100%;
  color: var(--signal-danger);
  font-weight: var(--weight-bold);
}

.item-price-error[hidden],
.category-rename-error[hidden] {
  display: none;
}

.item-row-reorder {
  flex-shrink: 0;
  display: flex;
  gap: var(--space-1);
}

/* ---- Desktop: the item row collapses to a single aligned line ------------
   On mobile the two-line split (identity + visibility/overflow above, price +
   reorder below) is right — 390px has no room for six things in a row, and
   the controls sit directly beside the content they act on.
   At 64rem the same structure fell apart: each line is `space-between` across
   a 40rem measure holding ~200px of content, so every row rendered its
   content at the far inline-start and a 2x2 blob of identical square buttons
   at the far inline-end, with ~240px of nothing between them — twice per row,
   at ~118px of height per row.
   `display: contents` dissolves the two line wrappers so their children
   become direct flex items of the row, and `order` lays them out as real
   columns: thumb, name (the one flexible column), price, reorder, controls.
   Rows align with each other, height halves, and the void is gone. The JS
   builds exactly the same DOM at every width — this is layout only, so the
   mobile path the e2e suite drives is untouched. */
@media (min-width: 64rem) {
  .item-row {
    flex-direction: row;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-3);
  }

  .item-row-top,
  .item-row-bottom {
    display: contents;
  }

  .item-row-thumb { order: 1; }

  .item-row-identity {
    order: 2;
    flex: 1;
  }

  .item-price-region { order: 3; }
  .item-row-reorder { order: 4; }
  .item-row-controls { order: 5; }


  /* The row-level error is a full-width line under the row, not a sixth
     column — it only appears when a price edit fails. */
  .item-row-error {
    order: 6;
    flex-basis: 100%;
  }

  /* servicePages' composition summary is likewise a full-width line under
     the row, not a column — without an explicit order it would default to
     0 and jump ahead of the thumbnail (order: 1). Same slot as
     .item-row-error (order: 6); document order breaks the tie since both
     are full-width lines that simply stack. */
  .list-row-compose {
    order: 6;
  }
}

/* ---- overflow sheet (تعديل / تكرار / شارة / حذف) ---- */

.sheet-backdrop {
  position: fixed;
  inset: 0;
  background: var(--scrim);
  z-index: var(--layer-sheet);
}

.sheet {
  position: fixed;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: var(--layer-sheet);
  max-width: var(--content-measure);
  margin-inline: auto;
  background: var(--surface-raised);
  border-start-start-radius: var(--radius-surface);
  border-start-end-radius: var(--radius-surface);
  box-shadow: var(--shadow-sheet);
  padding: var(--space-4) var(--space-4) calc(var(--space-4) + var(--safe-block-end));
}

.sheet-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-block-end: var(--space-2);
}

.sheet-title {
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--text-emphasis);
  line-height: var(--leading-title);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sheet-close {
  flex-shrink: 0;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: var(--ink-soft);
  cursor: pointer;
}

.sheet-actions {
  display: flex;
  flex-direction: column;
}

.sheet-action {
  display: flex;
  align-items: center;
  min-height: var(--row-min-height);
  padding-inline: var(--space-2);
  background: none;
  border: none;
  border-block-end: var(--hairline);
  font-weight: var(--weight-bold);
  font-size: var(--text-body);
  color: var(--ink);
  text-decoration: none;
  text-align: start;
  cursor: pointer;
}

.sheet-action:last-child {
  border-block-end: none;
}

.sheet-action--danger {
  color: var(--signal-danger);
}

/* ---- form-carrying sheet (list-editor.js, Phase 5 Task 3) — the same
   shell as the action sheet above (.sheet-backdrop/.sheet/.sheet-header),
   extended with a scrollable body so a field-heavy edit form (servicePages'
   seven fields + its steps mini-list) never pushes the close button off a
   short phone viewport. Scoped to this one modifier rather than changed on
   the base .sheet rule — the kebab action sheet above is always short and
   has no reason to grow scroll chrome. ---- */
.sheet--scrollable {
  display: flex;
  flex-direction: column;
  max-block-size: 85dvh;
}

.sheet--scrollable .sheet-header {
  flex-shrink: 0;
}

.sheet-body {
  overflow-y: auto;
  min-block-size: 0;
}

/* Shown only when a close attempt (✕/backdrop/Escape) is refused because
   the sheet's extras widget is still busy (list-editor.js, Phase 5 Task 4
   fix round — gallery: an upload in flight). Quiet, not alarming — nothing
   is wrong, the owner just needs to wait a couple seconds — so it reads
   like .field-hint, not .form-error. */
.sheet-busy-notice {
  flex-shrink: 0;
  color: var(--ink-soft);
  margin-block-end: var(--space-2);
}

/* ---- delete undo toast ---- */

.items-undo-toast {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-block-start: var(--space-4);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-inverse);
  color: var(--ink-on-inverse);
  border-radius: var(--radius-surface);
}

.items-undo-btn {
  flex-shrink: 0;
  min-height: var(--tap-min);
  padding-inline: var(--space-4);
  background: none;
  border: 1px solid var(--ink-on-inverse);
  border-radius: var(--radius-control);
  color: var(--ink-on-inverse);
  font-weight: var(--weight-bold);
  cursor: pointer;
}

/* ---- empty state — an invitation, not a dead end ---- */

.items-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-4);
  padding: var(--space-7) var(--space-5);
  background: var(--surface-raised);
  border: var(--hairline);
  border-radius: var(--radius-surface);
}

.items-empty-icon {
  color: var(--ink-soft);
}

.items-empty-text {
  font-weight: var(--weight-bold);
  font-size: var(--text-emphasis);
  max-width: 24rem;
}

.items-empty .items-add-btn {
  width: auto;
  padding-inline: var(--space-6);
}

/* ==========================================================================
   Item editor (item.html, screen 5 — cms.md §3 / DESIGN.md). Fields flow
   directly on the page ground as one continuous ledger page, hairline-
   divided per group — deliberately not a boxed "settings card" (DESIGN.md
   §3/§7: cards are for the few true objects, never the generic admin-form
   gesture). The priceMode segmented control and options editor are the
   screen's only bespoke controls; everything else reuses .field from the
   auth form and .item-switch from the items screen.
   ========================================================================== */

.item-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.item-form-back {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  color: var(--ink-soft);
  font-weight: var(--weight-bold);
  text-decoration: none;
}

.item-form-back:hover {
  color: var(--ink);
}

/* Power-cut draft guard's restore offer (Phase 7 Task 3, draft-guard.js's
   attachDraftGuard) — a HELPFUL OFFER, not a warning: deliberately neutral
   (jass + hairline, no signal hue at all — DESIGN.md §1 reserves kahraman
   for the one "unpublished changes" meaning, and this isn't that state).
   استرجاع/تجاهل read as quiet text actions (same inline-flex/tap-min/
   underline treatment as .publish-bar-link) rather than .btn pills, so
   neither one competes with the screen's own real save button below it.
   `margin-block-end` (not just the parent's own flex `gap`, where one
   exists — .item-form) because this bar is also inserted inside sheet
   bodies (list-editor.js), which lay out by margin, not gap — see
   .sheet-busy-notice's identical reasoning right above its own rule. */
.draft-guard-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
  padding: var(--space-4);
  margin-block-end: var(--space-4);
  background: var(--surface-raised);
  border: var(--hairline-strong);
  border-radius: var(--radius-surface);
}

.draft-guard-bar-text {
  color: var(--ink);
  font-weight: var(--weight-bold);
}

.draft-guard-bar-actions {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-shrink: 0;
}

.draft-guard-bar-restore,
.draft-guard-bar-discard {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding-inline: var(--space-2);
  background: none;
  border: none;
  font-size: var(--text-body);
  font-weight: var(--weight-bold);
  text-decoration: underline;
  text-decoration-thickness: from-font;
  cursor: pointer;
}

.draft-guard-bar-restore {
  color: var(--ink);
}

.draft-guard-bar-discard {
  color: var(--ink-soft);
}

.draft-guard-bar-restore:hover { color: color-mix(in srgb, var(--ink) 88%, black); }
.draft-guard-bar-discard:hover { color: var(--ink); }

.item-fields {
  display: flex;
  flex-direction: column;
}

.field-group {
  padding-block: var(--space-5);
  border-block-end: var(--hairline);
}

.field-group:first-child {
  padding-block-start: 0;
}

.field-group--row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.field-hint {
  color: var(--ink-soft);
}

/* Revert durability's skipped-images note on the tenant-detail suspend/
   unsuspend action (Phase 7 Task 2 review round, page-tenant.js's
   buildSuspendAction) — the light-surface amber-caution pairing (DESIGN.md
   §1's one amber-signal family), same as .tenant-reason--overdue /
   .rotation-warning-heading. NOT chrome.js's own --publish-live-note-ink
   (that token is tuned for TEXT on the dark zinjar publish-bar ground —
   this row sits on a light --surface-raised/--jass ground instead). */
.tenant-suspend-skipped-note {
  color: var(--kahraman-deep);
  font-weight: var(--weight-bold);
}

.item-form-hint-link {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  margin-block-start: var(--space-2);
  color: var(--ink);
  font-weight: var(--weight-bold);
  text-decoration: underline;
  text-decoration-thickness: from-font;
}

.field textarea,
.field select {
  min-height: var(--control-height);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-ground);
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  font-size: var(--text-body);
  font-family: inherit;
}

.field textarea {
  resize: vertical;
  line-height: var(--leading-body);
}

.field select {
  padding-block: 0;
}

.field textarea:focus-visible,
.field select:focus-visible {
  border-color: var(--kahraman-deep);
}

.field-error {
  color: var(--signal-danger);
  font-weight: var(--weight-bold);
}

.field-error[hidden] { display: none; }

/* ---- Add-tenant screen (new-tenant.html, Phase 6 Task 6) ---- */

/* siteId live-availability status line — neutral (ink-soft) while
   checking, zinjar when free, fuwwa when taken. Never the loud .form-error
   treatment for the "checking" state — that's not a problem, just not an
   answer yet. */
.field-status--ok {
  color: var(--zinjar);
  font-weight: var(--weight-bold);
}

.field-status--taken {
  color: var(--signal-danger);
  font-weight: var(--weight-bold);
}

/* The password field's show/hide toggle sits inline with its input —
   .field-ltr's own row alignment (input right-aligned, LTR direction)
   still applies to the input; the toggle is a plain icon button the same
   size class as .sheet-close, just inline instead of in a sheet header. */
.password-field-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.password-field-row input {
  flex: 1;
  min-width: 0;
}

.password-field-toggle {
  flex-shrink: 0;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  color: var(--ink-soft);
  cursor: pointer;
}

/* D14 per-city theme-rotation warning (docs/decisions.md D14) — reuses the
   tenant list's own "waiting, not trouble" amber pairing (.tenant-reason
   --overdue's kahraman-tint/kahraman-deep, DESIGN.md §1's one amber signal
   family) rather than inventing a second meaning for amber. A caution for
   agency judgment, never a block — there is no disabled state tied to it. */
.rotation-warning {
  padding: var(--space-4);
  background: var(--kahraman-tint);
  border-radius: var(--radius-surface);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.rotation-warning[hidden] { display: none; }

.rotation-warning-heading {
  color: var(--kahraman-deep);
  font-weight: var(--weight-bold);
}

.rotation-warning-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.rotation-warning-chip {
  padding-inline: var(--space-2);
  padding-block: 0.1em;
  background: var(--surface-raised);
  border-radius: var(--radius-control);
  color: var(--kahraman-deep);
  font-weight: var(--weight-bold);
}

/* ---- priceMode segmented control ---- */

.segmented {
  display: flex;
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  overflow: hidden;
}

.segmented-btn {
  flex: 1;
  min-height: var(--tap-min);
  padding: var(--space-1) var(--space-1);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-raised);
  border: none;
  border-inline-start: var(--hairline-strong);
  color: var(--ink);
  font-size: var(--text-body);
  font-weight: var(--weight-bold);
  line-height: var(--leading-control);
  text-align: center;
  cursor: pointer;
}

.segmented-btn:first-child {
  border-inline-start: none;
}

.segmented-btn[aria-checked="true"] {
  background: var(--basalt);
  color: var(--ink-on-inverse);
}

.price-fields {
  margin-block-start: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* ---- options editor (features.sizeOptions only) ---- */

.option-rows {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.option-row {
  padding: var(--space-3);
  background: var(--surface-raised);
  border: var(--hairline);
  border-radius: var(--radius-surface);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.option-row-header {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.option-label-input {
  flex: 1;
  min-height: var(--control-height);
  padding-inline: var(--space-3);
  background: var(--surface-ground);
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  font-size: var(--text-body);
  font-weight: var(--weight-bold);
}

.option-label-input:focus-visible {
  border-color: var(--kahraman-deep);
}

.option-remove-btn {
  flex-shrink: 0;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: var(--signal-danger);
  cursor: pointer;
}

.option-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  min-height: var(--tap-min);
  padding-inline: var(--space-3);
  background: var(--hajar);
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  color: var(--ink);
  font-weight: var(--weight-bold);
  line-height: var(--leading-control);
  cursor: pointer;
}

.chip:hover {
  background: var(--basalt-hairline-strong);
}

.option-add-value-row {
  display: flex;
}

.option-value-input {
  flex: 1;
  min-height: var(--control-height);
  padding-inline: var(--space-3);
  background: var(--surface-ground);
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  font-size: var(--text-body);
}

.option-value-input:focus-visible {
  border-color: var(--kahraman-deep);
}

.options-add-btn {
  margin-block-start: var(--space-4);
  width: 100%;
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  background: none;
  border: 1px dashed var(--basalt-hairline-strong);
  border-radius: var(--radius-control);
  color: var(--ink);
  font-weight: var(--weight-bold);
  line-height: var(--leading-control);
  cursor: pointer;
}

.options-add-btn:hover {
  background: var(--surface-raised);
}

/* ---- specs editor (features.itemSpecs only) ---- */

.spec-row { display: grid; grid-template-columns: 1fr 1fr auto; gap: 8px; align-items: center; margin-block-end: 8px; }
.spec-row .spec-row-error { grid-column: 1 / -1; margin: 0; }
.spec-row-remove { min-height: 48px; min-width: 48px; }
/* A bare <input> defaults its grid item to auto/min-content width — for a
   text input that's its `size` attribute (20ch), and TWO of them side by
   side blow past 390px before the row's own content ever gets a chance to
   wrap. Same fix as .password-field-row input elsewhere in this file, grid
   flavor: without it the row silently overflows the viewport on the phone
   widths this panel is built for. */
.spec-row input { min-width: 0; }

/* ---- badge quick-fill ---- */

.badge-field-row {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.badge-field-row input {
  flex: 1;
  min-width: 8rem;
}

.badge-quickfill {
  flex-shrink: 0;
  min-height: var(--tap-min);
  padding-inline: var(--space-4);
  display: flex;
  align-items: center;
  background: none;
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  color: var(--ink);
  font-weight: var(--weight-bold);
  line-height: var(--leading-control);
  cursor: pointer;
}

.badge-quickfill:hover {
  background: var(--surface-raised);
}

/* ---- photo (cms.md §3 "Photos", DESIGN.md §3: "cards … reserved for the
   few true objects — a photo") — the item's one card-shaped surface in an
   otherwise card-free ledger page. ---- */

.photo-field {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3);
}

.photo-hint {
  color: var(--ink-soft);
}

/* Empty state — a dashed invitation the same footprint as the eventual
   photo card, so picking a photo doesn't jump the layout. */
.photo-add {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  width: 100%;
  max-width: 12rem;
  aspect-ratio: 1;
  padding: var(--space-3);
  border: 1px dashed var(--basalt-hairline-strong);
  border-radius: var(--radius-surface);
  color: var(--ink-soft);
  text-align: center;
  cursor: pointer;
}

.photo-add:hover {
  background: var(--surface-raised);
}

.photo-add-label {
  font-weight: var(--weight-bold);
  color: var(--ink);
}

/* ---- has-a-photo state ---- */

.photo-card-wrap {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3);
  width: 100%;
}

.photo-card {
  width: 100%;
  max-width: 12rem;
  aspect-ratio: 1;
  border: var(--hairline);
  border-radius: var(--radius-surface);
  background: var(--surface-raised);
  overflow: hidden;
}

.photo-card img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.photo-card-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.photo-replace-label,
.photo-delete-btn {
  min-height: var(--tap-min);
  padding-inline: var(--space-4);
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  font-weight: var(--weight-bold);
  cursor: pointer;
}

.photo-replace-label {
  color: var(--ink);
}

.photo-replace-label:hover,
.photo-delete-btn:hover {
  background: var(--hajar);
}

/* Delete is destructive (DESIGN.md §1: fuwwa is failure/destructive-only). */
.photo-delete-btn {
  color: var(--signal-danger);
  border-color: var(--signal-danger);
}

/* ---- crop step — drag to reposition, slider to zoom. Physical (not
   logical) positioning inside the viewport on purpose: this is pixel-space
   canvas math driving a CSS transform, not page flow — same carve-out
   admin.css already documents for the publish bar's course direction. ---- */

.photo-crop {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  width: 100%;
}

.photo-crop-viewport {
  position: relative;
  width: 100%;
  max-width: 18rem;
  aspect-ratio: 1;
  overflow: hidden;
  border: var(--hairline-strong);
  border-radius: var(--radius-surface);
  background: var(--hajar);
  touch-action: none;
  cursor: grab;
}

.photo-crop-viewport:active {
  cursor: grabbing;
}

.photo-crop-img {
  position: absolute;
  left: 50%;
  top: 50%;
  max-width: none;
  pointer-events: none;
  user-select: none;
}

.photo-crop-zoom {
  display: flex;
  align-items: center;
  min-height: var(--tap-min);
  max-width: 18rem;
}

.photo-crop-zoom input[type="range"] {
  width: 100%;
}

.photo-crop-actions {
  display: flex;
  gap: var(--space-3);
  max-width: 18rem;
}

.photo-crop-actions .btn {
  flex: 1;
  width: auto;
}

/* ---- uploading — honest indeterminate state (uploadBytes has no progress
   events); the skeleton pulse is the same motion the auth-guard skeleton
   uses, never the publish bar's course (DESIGN.md §0: that pattern is
   memorable in exactly one place). ---- */

.photo-uploading {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

.photo-skeleton {
  flex-shrink: 0;
  width: 5rem;
  height: 5rem;
  border-radius: var(--radius-surface);
  background: var(--basalt-hairline);
  animation: skeleton-pulse 1400ms var(--ease-out) infinite;
}

@media (prefers-reduced-motion: reduce) {
  .photo-skeleton { animation: none; opacity: 0.75; }
}

.photo-uploading-headline {
  font-weight: var(--weight-bold);
}

.photo-uploading-meta {
  margin-block-start: var(--space-1);
  color: var(--ink-soft);
}

/* ---- failure — sacred-path tier (CONSTITUTION.md): named problem, retry,
   never vague. Reuses .form-error (auth card). ---- */

.photo-error-actions {
  display: flex;
  gap: var(--space-3);
  margin-block-start: var(--space-3);
  max-width: 18rem;
}

.photo-error-actions .btn {
  flex: 1;
  width: auto;
}

.item-form-save {
  margin-block-start: var(--space-2);
}

/* ==========================================================================
   Categories screen (categories.html, screen 6 — cms.md §3 / DESIGN.md).
   Same row shell as the items screen (full-bleed hairline rows, top/bottom
   split); rename swaps the name for an inline input exactly like the price
   editor's swap-in-place, just paired with an explicit pencil affordance
   next to the tap-to-edit name since a category has no other control that
   already reads as "the editable thing" the way an item's price does.
   ========================================================================== */

.categories-list {
  border-block-start: var(--hairline);
}

.category-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding-block: var(--space-3);
  padding-inline: var(--space-1);
  border-block-end: var(--hairline);
}

.category-row[data-visible="false"] {
  opacity: 0.55;
}

.category-row-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
}

.category-row-identity {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.category-row-name-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1);
  min-width: 0;
}

/* A button styled as the row title (not a <p>) — tapping the name is the
   same commit gesture as tapping the pencil beside it, so it earns the
   same ≥48px hit box (quality floor, DESIGN.md §6). The negative margin
   cancels the padding that buys the height/width so the visible text
   still sits where a plain, unpadded label would. */
.category-row-name {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  min-width: var(--tap-min);
  padding-inline: var(--space-1);
  margin-inline: calc(var(--space-1) * -1);
  border-radius: var(--radius-control);
  background: none;
  border: none;
  font: inherit;
  text-align: start;
  color: inherit;
  font-weight: var(--weight-bold);
  word-break: break-word;
  cursor: pointer;
}

.category-row-name:hover {
  background: var(--hajar);
}

.category-row-count {
  color: var(--ink-soft);
}

.category-row-controls {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

.category-row-bottom {
  display: flex;
  justify-content: flex-end;
}

/* Desktop: same single-line collapse the item row gets, for the same reason —
   this row carries a name, a count, three controls and two reorder buttons
   across three stacked lines and a ~250px void, when all of it fits on one.
   See the .item-row desktop block for the mechanics. */
@media (min-width: 64rem) {
  .category-row {
    flex-direction: row;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-3);
  }

  .category-row-top,
  .category-row-bottom {
    display: contents;
  }

  .category-row-identity {
    order: 1;
    flex: 1;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-3);
  }

  .category-row-reorder { order: 2; }
  .category-row-controls { order: 3; }

  .category-row .item-row-error {
    order: 4;
    flex-basis: 100%;
  }

  /* The inline rename editor replaces the identity wrapper's children, so
     with identity laid out as a row it needs to claim the line and drop its
     error message underneath rather than beside itself. */
  .category-rename-edit { flex: 1; }
  .category-rename-error { flex-basis: 100%; }
}

.category-row-reorder {
  flex-shrink: 0;
  display: flex;
  gap: var(--space-1);
}

/* ==========================================================================
   Sections index (site.html, Phase 5 Task 1 — cms/CLAUDE.md phase table /
   DESIGN.md §3). Same "rows, not cards" workbench surface as items/
   categories: full-bleed hairline rows, a quiet leading icon (wayfinding,
   never decoration — DESIGN.md §4), name + one-line description, trailing
   chevron. No per-row controls here (toggling/reordering happens inside
   each section's own screen), so the whole row is one tap target.
   ========================================================================== */

.site-screen {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.site-section-list {
  border-block-start: var(--hairline);
}

/* The shop-name row at the top of الموقع (owner-feedback round A, item 2).
   Deliberately NOT styled as one more .site-section-row: it is an identity
   line, not a section. No margin — .site-screen is a flex column with its
   own gap.

   De-emphasis here is COLOUR, never size: tokens.css sets --text-body to
   "16px — the floor; nothing renders smaller", and this panel is read on
   cheap phones in daylight. --ink-soft is the same de-emphasis
   .site-section-desc already uses. */
.site-name-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: var(--row-min-height);
  padding-block: var(--space-3);
  padding-inline: var(--space-4);
  border: var(--hairline);
  border-radius: var(--radius-control);
  color: inherit;
  text-decoration: none;
}

.site-name-label {
  color: var(--ink-soft);
}

/* A long name (the field allows 80 chars) must not push «تعديل» out of the
   row — wrap instead, the row grows. */
.site-name-value {
  font-weight: var(--weight-bold);
  overflow-wrap: anywhere;
}

.site-name-edit {
  flex: none;
  text-decoration: underline;
}

.site-section-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--row-min-height);
  padding-block: var(--space-3);
  padding-inline: var(--space-1);
  border-block-end: var(--hairline);
  color: inherit;
  text-decoration: none;
}

.site-section-row:hover {
  background: var(--surface-raised);
}

.site-section-icon {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  color: var(--ink-soft);
  background: var(--hajar);
  border-radius: var(--radius-control);
}

.site-section-text {
  flex: 1;
  min-width: 0;
}

.site-section-label {
  font-weight: var(--weight-bold);
  color: var(--ink);
}

.site-section-desc {
  margin-block-start: var(--space-1);
  color: var(--ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.site-section-chevron {
  flex-shrink: 0;
  color: var(--ink-soft);
}

/* ==========================================================================
   Section-form engine (section-form.js, Phase 5 Task 1) — the field shell
   every Task 2-5 section screen shares. Reuses .field/.field-group/
   .field-error (item editor) and .item-switch (items/categories) rather
   than inventing a parallel set of form primitives; .section-form-save
   only exists to give the save button the same block-start breathing room
   item-form-save gives the item editor's.
   ========================================================================== */

.section-form-save {
  margin-block-start: var(--space-2);
}

/* ==========================================================================
   Object-section screens (page-section.js, Phase 5 Task 2) — home/about/
   contact/hours+delivery/banner (section.html). Reuses the option-row/chip
   primitives above almost entirely (mountMiniList, mountChipList in
   page-section.js); the two rules below are the only genuinely new shapes
   this task needed — a header row for a mini-list card that holds nothing
   but its own remove button, and the fixed 7-row hours table (no existing
   list shape fits a day name + two time inputs + a switch).
   ========================================================================== */

.mini-list-row-header {
  display: flex;
  justify-content: flex-end;
}

.hours-rows {
  margin-block-start: var(--space-2);
}

.hours-row {
  padding-block: var(--space-3);
  border-block-end: var(--hairline);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.hours-row:last-child {
  border-block-end: none;
}

.hours-row-day {
  font-weight: var(--weight-bold);
}

.hours-row-controls {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.hours-row-times {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.hours-time-input {
  flex: 1;
  min-width: 0;
  min-height: var(--control-height);
  padding-inline: var(--space-2);
  background: var(--surface-ground);
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  font-size: var(--text-body);
}

.hours-time-input:focus-visible {
  border-color: var(--kahraman-deep);
}

.hours-time-input:disabled {
  opacity: 0.5;
}

.hours-row-dash {
  flex-shrink: 0;
  color: var(--ink-soft);
}

/* Desktop only: the day name has room to sit beside its controls instead of
   stacking above them (DESIGN.md §3 — desktop is an enhancement of the
   phone layout, same list, never a different product). */
@media (min-width: 64rem) {
  .hours-row {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
  }

  .hours-row-controls {
    flex: 0 0 auto;
  }

  .hours-time-input {
    flex: 0 0 8rem;
  }
}

/* ==========================================================================
   List editor (list-editor.js, Phase 5 Task 3) — testimonials/faq/packages/
   servicePages. Rows reuse .item-row/.item-row-top/.item-row-identity/
   .item-row-controls/.item-row-bottom/.item-row-reorder/.item-visibility/
   .item-switch verbatim (list-editor-edit-btn/list-editor-delete-btn join
   the shared icon-button rule above) — the only genuinely new shape is the
   one line of section-specific content under the row name.
   ========================================================================== */

.list-editor-row-content {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--ink-soft);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

/* packages: price + badge sit inline, ledger-numeral price first (DESIGN.md
   §3: "price sitting beside the name where it's read") — not clamped, since
   this line is never running prose. */
.list-editor-row-content--inline {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

/* servicePages composition summary (2026-08-06 spec Part 3,
   renderServicePageRow) — a second muted line under the row's own price/
   intro line, present only when at least one composition field
   (itemsCategoryId/showPackages/addItemId) is set. flex-basis:100% is the
   same idiom .item-row-error already uses just below: always its own full-
   width line rather than competing with .list-editor-row-content for the
   row's flex space. */
.list-row-compose {
  flex-basis: 100%;
  min-width: 0;
  color: var(--ink-soft);
  font-size: 0.85rem;
}

/* Desktop single-line rows (the ≥64rem .item-row collapse above dissolves
   .item-row-top/.item-row-bottom with display:contents): the content line
   becomes a direct flex item of .item-row, but that collapse only assigned
   `order` to the ITEMS screen's children — this line defaulted to order 0
   (sorted before the name) and its auto flex-basis starved
   .item-row-identity's basis-0 column down to a letter-wide strip, so the
   row name rendered vertically one character per line (live finding on the
   servicePages screen, 2026-08-05). Slot it where the items screen's price
   column sits, zero basis so name:content split the free space 1:2 and rows
   align as columns. Must live AFTER the base rule above — same specificity,
   and the base `flex: 1 1 auto` would win the cascade from below the media
   query. */
@media (min-width: 64rem) {
  .list-editor-row-content {
    order: 3;
    flex: 2 1 0;
  }
}

/* ==========================================================================
   themeExtras editor (themeextras-editor.js, /section?s=themeExtras —
   agency-only, 2026-08-05 spec). One raw-JSON textarea: LTR monospace
   island inside the RTL panel (same convention as the publish bar's error
   detail lines), tall enough to hold a whole theme's extras without
   scrolling on desktop. The error line stacks the Arabic headline over the
   Latin validator lines — pre-line keeps the \n splits, LTR keeps the
   /themeExtras/... paths readable.
   ========================================================================== */

.themeextras-textarea {
  min-height: 24rem;
  padding: var(--space-3) var(--space-4);
  background: var(--surface-ground);
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.85rem;
  line-height: 1.55;
  direction: ltr;
  text-align: left;
  resize: vertical;
  overflow-wrap: anywhere;
}

.themeextras-textarea:focus-visible {
  border-color: var(--kahraman-deep);
}

.themeextras-error-lines {
  margin: var(--space-2) 0 0;
  padding-inline-start: var(--space-4);
  list-style: disc;
  direction: ltr;
  text-align: left;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.8rem;
  color: var(--signal-danger);
  overflow-wrap: anywhere;
}

/* ==========================================================================
   themeExtras generated form (themeextras-editor.js, 2026-08-06 spec Part
   2) — the same JSON the textarea above holds, rendered as RTL-friendly
   fields; JSON keys stay LTR monospace labels (same convention as
   .themeextras-textarea/-error-lines above). Deliberately reuses the
   panel's own existing idioms rather than inventing a new component
   language for what is a generated VIEW: tabs = .items-chip's
   pressed/unpressed pair, the "الفعّال" badge = .tenant-status--active's
   quiet zinjar outline, the "بقايا" badge = .tenant-reason--overdue's
   kahraman-tint caution, inputs = .field input's 16px floor. No --line/
   --card custom properties exist in this file (tokens.css is the source of
   truth) — every value below is one of tokens.css's own, not a literal. */
.tx-tabs {
  display: flex;
  gap: var(--space-2);
  margin-block-end: var(--space-3);
}

.tx-tab {
  min-height: var(--tap-min);
  padding-inline: var(--space-4);
  background: var(--surface-raised);
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  font: inherit;
  font-weight: var(--weight-bold);
  line-height: var(--leading-control);
  color: var(--ink-soft);
  cursor: pointer;
}

.tx-tab-active {
  background: var(--basalt);
  border-color: var(--basalt);
  color: var(--ink-on-inverse);
}

.tx-tab:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

.tx-slug {
  border: var(--hairline-strong);
  border-radius: var(--radius-surface);
  padding: var(--space-1) var(--space-4) var(--space-4);
  margin-block-end: var(--space-3);
}

.tx-slug > summary {
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
  gap: var(--space-3);
  cursor: pointer;
}

.tx-key {
  direction: ltr;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.85rem;
  color: var(--ink-soft);
}

.tx-group-key {
  display: block;
  margin-block: var(--space-3) var(--space-1);
  font-weight: var(--weight-bold);
  color: var(--ink);
}

.tx-badge {
  flex-shrink: 0;
  padding-inline: var(--space-2);
  padding-block: 0.1em;
  border-radius: var(--radius-control);
  font-weight: var(--weight-bold);
}

.tx-badge-active {
  background: transparent;
  color: var(--zinjar);
  border: 1px solid color-mix(in srgb, var(--zinjar) 40%, transparent);
}

.tx-badge-stale {
  background: var(--kahraman-tint);
  color: var(--kahraman-deep);
}

.tx-group {
  border-inline-start: var(--hairline);
  padding-inline-start: var(--space-3);
  margin-block: var(--space-2);
}

.tx-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin-block: var(--space-3);
}

.tx-input {
  min-height: var(--control-height);
  padding-inline: var(--space-4);
  padding-block: var(--space-2);
  background: var(--surface-ground);
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  font-size: var(--text-body); /* 16px floor — never smaller, never zooms iOS */
  font-family: inherit;
}

.tx-input:focus-visible {
  border-color: var(--kahraman-deep);
}

textarea.tx-input {
  min-height: 4.5rem;
  resize: vertical;
  line-height: var(--leading-body);
}

.tx-input-num {
  max-inline-size: 9rem;
}

/* Fix round 1: boolean fields had no rule at all — bare browser checkbox,
   no --tap-min target, no focus ring. No existing checkbox idiom to match
   (grepped admin.css: this is the panel's first native checkbox), so this
   mirrors .tx-input's own treatment token-for-token: tap-min sizing instead
   of the ~13px UA default, the same hairline-strong/radius-control border
   pair, --basalt for the native tick (accent-color, same ink .btn-primary
   fills with), and .tx-input's own kahraman-deep border-color focus-visible
   swap (not an outline ring — matching .tx-input/.field input verbatim). */
.tx-check {
  width: var(--tap-min);
  height: var(--tap-min);
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  accent-color: var(--basalt);
  cursor: pointer;
}

.tx-check:focus-visible {
  border-color: var(--kahraman-deep);
}

.tx-opaque {
  font-size: 0.85rem;
  color: var(--ink-soft);
  direction: ltr;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}

.tx-array-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin-block: var(--space-2);
}

.tx-array-row > :first-child {
  flex: 1;
}

.tx-row-del,
.tx-row-add {
  flex-shrink: 0;
  min-height: var(--tap-min);
  padding-inline: var(--space-4);
  background: transparent;
  border: var(--hairline-strong);
  border-radius: var(--radius-control);
  font: inherit;
  font-weight: var(--weight-bold);
  line-height: var(--leading-control);
  cursor: pointer;
}

.tx-row-del {
  color: var(--signal-danger);
}

/* ==========================================================================
   Settings screen (page-section.js mountSettingsScreen, Phase 5 Task 5 —
   cms.md screen 17). The identity+SEO fields flow as the same hairline
   .item-fields/.field-group ledger every other section screen uses above;
   change-password and help are NOT schema-backed sections, so each gets its
   own lightweight .settings-block instead — same hairline rhythm as a
   field-group, its Markazi heading the one place a sub-heading borrows the
   title voice (DESIGN.md §2: "used only for screen titles ... and section
   headings"), never a boxed card (DESIGN.md §3: cards are for the few true
   objects, never the generic admin-form gesture).
   ========================================================================== */

.settings-block {
  padding-block: var(--space-5);
  border-block-end: var(--hairline);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.settings-block:last-child {
  border-block-end: none;
}

.settings-block-title {
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--text-emphasis);
  line-height: var(--leading-title);
}

/* Owner-facing section switches (D37 — page-section.js
   buildFeatureFlagsBlock). The whole ROW is the <label>, so the whole row is
   the tap target: a bare checkbox is far under the --tap-min floor, and this
   panel is used one-handed on a phone.

   De-emphasis is colour, never size — tokens.css keeps 16px as the floor
   ("nothing renders smaller"), same stance .site-name-label takes. */
.settings-block-note {
  color: var(--ink-soft);
}

.feature-flag-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  min-height: var(--tap-min);
  padding-block: var(--space-2);
  cursor: pointer;
}

.feature-flag-input {
  flex: none;
  /* Big enough to hit on its own even though the row is the target. */
  inline-size: 1.5rem;
  block-size: 1.5rem;
  margin-block-start: 0.125rem;
}

.feature-flag-hint {
  color: var(--ink-soft);
}

.settings-password-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.settings-help-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--tap-min);
  color: var(--ink);
  font-weight: var(--weight-bold);
  text-decoration: none;
}

.settings-help-link:hover {
  color: var(--ink-soft);
}

/* --------------------------------------------------------------------------
   Art picker (art-picker.js — Round B). Replaces the free-text icon box that
   asked an owner to guess ids like "roast" or "saw". A real radio group in a
   fieldset, so arrow-key navigation, screen-reader grouping and inertness
   inside form-busy's disabled fieldset all come free.
   -------------------------------------------------------------------------- */

.art-picker {
  border: 0;
  margin: 0;
  padding: 0;
  min-inline-size: 0; /* the UA default breaks grid children */
}

.art-picker-legend {
  padding: 0;
  margin-block-end: var(--space-2);
}

.art-picker-grid {
  display: grid;
  /* Fills the row at 390px and does not stretch to absurd widths on desktop. */
  grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr));
  gap: var(--space-2);
}

/* The whole tile is the label, so the whole tile is the tap target — a bare
   radio is far under the --tap-min floor. */
.art-option {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  min-block-size: var(--tap-min);
  padding: var(--space-2);
  border: var(--hairline);
  border-radius: var(--radius-control);
  cursor: pointer;
  text-align: center;
}

/* The radio itself stays in the accessibility tree and keyboard order — it is
   only visually replaced by the tile's own selected state. Never display:none,
   which would take it out of both. */
.art-option-input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.art-option:has(.art-option-input:checked) {
  border-color: var(--basalt-hairline-strong);
  box-shadow: inset 0 0 0 1px var(--basalt-hairline-strong);
}

/* Keyboard focus must be visible even though the input is transparent. */
.art-option:has(.art-option-input:focus-visible) {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

.art-option-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-block-size: 2.25rem;
}

.art-option-mark svg {
  inline-size: auto;
  block-size: 2rem;
  max-inline-size: 100%;
}

/* De-emphasis by colour, never size — tokens.css keeps 16px as the floor. */
.art-option-label {
  color: var(--ink-soft);
}

/* A stored id the theme no longer declares. Kept and shown rather than
   silently dropped, because dropping it would edit the owner's data just by
   rendering the screen. */
.art-option-unknown {
  border-style: dashed;
}

/* Extra product photos (items[].images[] — extra-photos-field.js). A wrapping
   row of thumbnails plus one `multiple` picker: several photos in a single
   gesture, no crop step (the swipe strip already object-fit:covers them). */

.extra-photos {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-block-end: var(--space-3);
}

.extra-photo-slot {
  position: relative;
  inline-size: 5.5rem;
  block-size: 5.5rem;
  border: var(--hairline);
  border-radius: var(--radius-control);
  overflow: hidden;
}

.extra-photo-thumb {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  display: block;
}

/* Sits ON the thumbnail, so the row stays compact at 390px. Full tap size
   even though it overlaps the image. */
.extra-photo-remove {
  position: absolute;
  inset-block-start: 0;
  inset-inline-end: 0;
  inline-size: var(--tap-min);
  block-size: var(--tap-min);
  border: 0;
  background: rgb(255 255 255 / 82%);
  cursor: pointer;
  line-height: 1;
}

.extra-photos-busy {
  color: var(--ink-soft);
  align-self: center;
}

.extra-photos-add {
  align-self: flex-start;
}

/* The category's tile art, shown in its row (2026-08-08 audit). Before this
   the row was name-only, so nothing on screen told an owner their categories
   HAD a picture — the picker existed but was effectively invisible. */
.category-row-art {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--tap-min);
  block-size: var(--tap-min);
  padding: 0;
  border: var(--hairline);
  border-radius: var(--radius-control);
  background: none;
  cursor: pointer;
}

.category-row-art svg {
  inline-size: 1.75rem;
  block-size: 1.75rem;
}
