/* =============================================================================
   Superservice Design System
   Canonical reference: docs/design/design-system.md
   Direction: minimal, polished, spacious, fast. Apple-like clarity,
   Telegram-like simplicity. Light-first with an automatic dark theme.
   Edit tokens here; do not hardcode colors/spacing in templates.
   ========================================================================== */

/* ---- Tokens ---------------------------------------------------------------*/
:root {
  color-scheme: light;

  /* Brand.
     These are the *fallback* only. The real accent is set per market and per domain
     in admin, and `apps/core/theming.py` derives hover, pressed, tint, on-accent and
     the whole dark ramp from that one value into a <style> block in <head>. Editing
     the values here changes what an unconfigured market looks like and nothing else.

     Blue, after a detour. The turquoise tried on 2026-08-04 is now a preset rather
     than the default: *"currently I don't like this green-ish primary color, the
     buttons and some icons are looking old, I liked the blue more"*. */
  --accent: #0071e3;
  --accent-hover: #0077ed;
  --accent-press: #0064c8;
  --accent-soft: rgba(0, 113, 227, 0.10);
  --on-accent: #ffffff;

  /* Saffron, the warm counterpoint. Reserved for money and for the one number on
     a screen that should be read before the others. Used anywhere else it stops
     meaning anything. */
  --gold: #a76a12;
  --gold-soft: rgba(167, 106, 18, 0.12);

  /* ---- Neutral surfaces: three layers, not two ----------------------------
     The owner, 2026-08-12: *"everything is just white"*. Literally true, and this
     was the line that made it so — `--bg` was `#fbfbfd` and `--surface` was
     `#ffffff`, a 1.5% difference. Every card on the site was white on white,
     separated only by a whisper of shadow, which is why the product read as flat
     and washed however carefully the boxes were arranged.

     Apple's answer is not a shadow. It is a **grouped background**: the page is
     tinted and the cards are white, and that contrast is the entire separation —
     iOS Settings uses no shadow at all. `#F2F2F7` is that colour, and it has been
     sitting in this file since 2026-08-04 as `--page-tint`, applied to exactly one
     element (the settings sheet). It is the page now.

     Three layers, matching `systemGroupedBackground` / `secondary` / `tertiary`:

       --bg         the page              tinted
       --surface    a card on the page    white
       --surface-3  content inside a card back to the tint

     Dark inverts the *lightness* but keeps the roles: the page is true black (which
     is also what an OLED phone wants) and cards lift off it. `#1c1c1e` rather than
     the old `#111114` — the old value was too close to black to read as raised. */
  --bg: #f2f2f7;
  --surface: #ffffff;
  --surface-2: #f5f5f7;
  --surface-3: #f2f2f7;
  --border: rgba(0, 0, 0, 0.08);
  --border-strong: rgba(0, 0, 0, 0.16);
  --overlay: rgba(0, 0, 0, 0.45);

  /* ---- Fills: the background of a control, not of a surface ----------------
     Apple's `systemFill` family. Deliberately *translucent grey* rather than a
     solid, so one value works on a white card, on a tinted page and on a coloured
     header without a variant for each. Four weights, thickest first — the thicker
     the fill, the smaller the shape it belongs on. */
  --fill-1: rgba(120, 120, 128, 0.20);
  --fill-2: rgba(120, 120, 128, 0.16);
  --fill-3: rgba(120, 120, 128, 0.12);
  --fill-4: rgba(120, 120, 128, 0.08);

  /* ---- Text: four levels, matching Apple's label hierarchy -----------------
     `--text-faint` is the fourth (`quaternaryLabel`): placeholder glyphs, a
     disabled row, the separator-adjacent text that must be present but must not be
     read. Without it, anything that quiet was borrowing `--text-subtle` and
     competing with real secondary text. */
  --text: #1d1d1f;
  --text-muted: #6e6e73;
  --text-subtle: #86868b;
  --text-faint: rgba(60, 60, 67, 0.30);

  /* Feedback */
  --success: #1d8a4e;
  --success-soft: rgba(29, 138, 78, 0.12);
  --danger: #d70015;
  --danger-soft: rgba(215, 0, 21, 0.10);
  /* Attention that is not an error. Three of the owner panel's row states are
     "not finished yet" — a section with no bio, a calendar with no hours, a
     service that is off — and until 2026-08-12 they all borrowed `--danger`,
     which tells someone their draft is broken. Apple's own amber, so it reads
     as a nudge rather than an alarm. */
  --warning: #b25e00;
  --warning-soft: rgba(178, 94, 0, 0.12);

  /* ---- The system palette --------------------------------------------------
     Apple's system colours, and the reason iOS Settings is navigable at a glance:
     every row carries a small tinted glyph, and you find Wi-Fi by its blue square
     long before you read the word. That is not decoration. It is the index.

     Before 2026-08-12 this product had an accent, a gold and three feedback
     colours, so the two rows that *were* tinted (Balance, Alerts) used gold and a
     hardcoded `#cf6b1e`. Everything else was a grey glyph on a white row, which is
     a list you have to read line by line.

     Rules, so this does not decay into a paint box:
     - A colour belongs to a **kind of thing**, permanently. Services are blue
       wherever services appear. Changing one means changing it everywhere.
     - The tint is for the **glyph tile only** — never a row background, never text.
       A coloured tile on a white row is legible; coloured text is not.
     - `--accent` stays separate and stays the market's own. It means *"this is the
       action"*; a system colour means *"this is that kind of thing"*. They are
       different jobs and a market that picks a green accent must not silently
       recolour the "money" tile.
     - Add to this list rather than inventing a colour at the call site. */
  --sys-blue: #007aff;
  --sys-green: #34c759;
  --sys-indigo: #5856d6;
  --sys-orange: #ff9500;
  --sys-pink: #ff2d55;
  --sys-purple: #af52de;
  --sys-red: #ff3b30;
  --sys-teal: #30b0c7;
  --sys-yellow: #ffcc00;
  --sys-brown: #a2845e;
  --sys-mint: #00c7be;
  --sys-gray: #8e8e93;

  /* Typography */
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
    Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
  --step--1: clamp(0.82rem, 0.79rem + 0.15vw, 0.9rem);
  --step-0: clamp(0.95rem, 0.92rem + 0.18vw, 1.05rem);
  --step-1: clamp(1.12rem, 1.04rem + 0.4vw, 1.35rem);
  --step-2: clamp(1.4rem, 1.25rem + 0.75vw, 1.85rem);
  --step-3: clamp(1.8rem, 1.5rem + 1.5vw, 2.6rem);
  --step-4: clamp(2.4rem, 1.9rem + 2.6vw, 3.75rem);
  --step-5: clamp(3rem, 2.2rem + 4vw, 5rem);

  /* Spacing scale (4px base) */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 2.75rem;
  --space-8: 3.5rem;
  --space-9: 5rem;
  --space-10: 7rem;

  /* Radius */
  --radius-sm: 10px;
  --radius: 16px;
  --radius-lg: 24px;
  --radius-xl: 32px;
  --radius-pill: 999px;

  /* Shadows (soft, low-opacity) */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 3px rgba(0, 0, 0, 0.06);
  --shadow: 0 10px 30px rgba(0, 0, 0, 0.06), 0 2px 8px rgba(0, 0, 0, 0.04);
  --shadow-lg: 0 24px 60px rgba(0, 0, 0, 0.10), 0 6px 18px rgba(0, 0, 0, 0.06);

  /* Layout + motion */
  --container: 1120px;
  --gutter: clamp(1.25rem, 0.8rem + 2vw, 2rem);
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --dur: 200ms;
  --ring: 0 0 0 4px var(--accent-soft);
}

@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;
    /* Lifted well past the light value: the same hex on black reads muddy, and a
       dark theme needs its accent to be the brightest thing on the screen. Derived
       per market by `theming.ramp()`; these are the unconfigured fallback. */
    --accent: #409fff;
    --accent-hover: #5faeff;
    --accent-press: #2190ff;
    --accent-soft: rgba(64, 159, 255, 0.16);
    --on-accent: #101418;

    --gold: #e9a94a;
    --gold-soft: rgba(233, 169, 74, 0.16);

    --bg: #000000;
    --surface: #1c1c1e;
    --surface-2: #2c2c2e;
    --surface-3: #2c2c2e;
    --border: rgba(255, 255, 255, 0.10);
    --border-strong: rgba(255, 255, 255, 0.20);

    /* The same four fills. Apple lifts their alpha in dark, because a translucent
       grey over black barely registers at the light-mode weights. */
    --fill-1: rgba(120, 120, 128, 0.36);
    --fill-2: rgba(120, 120, 128, 0.32);
    --fill-3: rgba(120, 120, 128, 0.24);
    --fill-4: rgba(120, 120, 128, 0.18);

    --text: #f5f5f7;
    --text-muted: #a1a1a6;
    --text-subtle: #86868b;
    --text-faint: rgba(235, 235, 245, 0.30);

    --success: #40c463;
    --success-soft: rgba(64, 196, 99, 0.16);
    --danger: #ff5a5f;
    --danger-soft: rgba(255, 90, 95, 0.14);
    --warning: #ffb340;
    --warning-soft: rgba(255, 179, 64, 0.16);

    /* Apple's dark variants — the same hues, lifted, because the light-mode values
       lose their identity against black. */
    --sys-blue: #0a84ff;
    --sys-green: #30d158;
    --sys-indigo: #5e5ce6;
    --sys-orange: #ff9f0a;
    --sys-pink: #ff375f;
    --sys-purple: #bf5af2;
    --sys-red: #ff453a;
    --sys-teal: #40c8e0;
    --sys-yellow: #ffd60a;
    --sys-brown: #ac8e68;
    --sys-mint: #63e6e2;
    --sys-gray: #98989d;

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
    --shadow: 0 12px 34px rgba(0, 0, 0, 0.5);
    --shadow-lg: 0 28px 64px rgba(0, 0, 0, 0.6);
  }
}

/* ---- Base -----------------------------------------------------------------*/
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--step-0);
  line-height: 1.55;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4 { margin: 0; line-height: 1.08; letter-spacing: -0.02em; font-weight: 650; }
/* An unbroken word at this size overflows a phone — profile names and fallback email
   addresses can both lack a convenient break opportunity. */
h1 { font-size: var(--step-4); letter-spacing: -0.04em; overflow-wrap: anywhere; }
h2 { font-size: var(--step-2); }
h3 { font-size: var(--step-1); }
/* A content block's own title. It is an `h4` since the profile page grew a real
   outline — `h1` business, `h2` niche, `h3` group, `h4` the thing — but it is still
   the heading of a paragraph someone wrote, so it keeps the size it had. */
.block__heading { font-size: var(--step-1); }
p { margin: 0; }

a { color: var(--accent); text-decoration: none; transition: color var(--dur) var(--ease); }
a:hover { color: var(--accent-hover); }

img { max-width: 100%; height: auto; display: block; }

:focus-visible { outline: none; box-shadow: var(--ring); border-radius: var(--radius-sm); }

.skip-link {
  /* Off-screen until focused. `inset-inline-start` (not `left`) so it lands on
     the correct side in RTL; no shadow while hidden or it bleeds into view. */
  position: absolute; inset-inline-start: var(--space-4); top: -3rem;
  background: var(--surface); color: var(--text); padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-sm); z-index: 100;
  transition: top var(--dur) var(--ease);
}
.brand__logo { display: block; height: 1.75rem; width: auto; max-width: 12rem; object-fit: contain; }
.skip-link:focus { top: var(--space-4); box-shadow: var(--shadow); }

/* ---- Layout ---------------------------------------------------------------*/
.wrap { width: 100%; max-width: var(--container); margin-inline: auto; padding-inline: var(--gutter); }
.section { padding-block: var(--space-8); }
.section--tight { padding-block: var(--space-6); }

.stack > * + * { margin-top: var(--space-4); }
.stack-lg > * + * { margin-top: var(--space-6); }

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

.grid { display: grid; gap: var(--space-5); grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr)); }

.eyebrow {
  display: inline-flex; align-items: center; gap: var(--space-2);
  font-size: var(--step--1); font-weight: 600; letter-spacing: 0.02em;
  color: var(--text-muted); text-transform: none;
}
.muted { color: var(--text-muted); }
.subtle { color: var(--text-subtle); }
.lead { font-size: var(--step-1); color: var(--text-muted); line-height: 1.5; }

/* ---- Navigation -----------------------------------------------------------*/
.nav {
  /* Above .filter-bar (z-index 30): the nav creates a stacking context, so the
     open-menu scrim inside it can only cover the page if the whole header does. */
  position: sticky; top: 0; z-index: 50;
  /* No backdrop-filter here: it makes the header a containing block for
     position:fixed descendants, which clipped the full-screen menu scrim to the
     header's own 56px. Fully opaque rather than the 94% it used to be — without a
     blur behind it, 6% transparency just showed page content sliding through the
     menu while scrolling. */
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}
.nav-inner { min-height: 56px; display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); }
.brand { font-weight: 700; letter-spacing: -0.03em; font-size: var(--step-1); color: var(--text); white-space: nowrap; }

/* Nav: state lives in a hidden checkbox (see base.html for why not <details>).
   Mobile — links collapse into a panel behind the burger. Desktop — burger hidden,
   links inline, subcategories on hover/focus. */
.nav-burger {
  display: inline-flex; align-items: center; justify-content: center;
  width: 3rem; height: 3rem; cursor: pointer;
  border-radius: var(--radius-sm); color: var(--text);
}
.nav-burger .icon { width: 1.6em; height: 1.6em; }
.nav-burger:hover { background: var(--surface-2); }
.nav-toggle:focus-visible + .nav-burger { outline: none; box-shadow: var(--ring); }

/* Mobile menu is a near-full-width sheet with generous rows. A cramped dropdown
   is hard for anyone with larger fingers, reduced vision or an older device —
   and this same markup ships inside the native app wrappers. */
.nav-links {
  position: absolute; z-index: 40;
  inset-inline: var(--space-4); top: calc(100% + var(--space-2));
  display: none; flex-direction: column; align-items: stretch; gap: var(--space-1);
  max-height: 75vh; overflow-y: auto; padding: var(--space-3);
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg); box-shadow: var(--shadow);
}
.nav-toggle:checked ~ .nav-links { display: flex; }

.nav-scrim { display: none; position: fixed; inset: 0; z-index: 35; cursor: default; }
.nav-toggle:checked ~ .nav-scrim { display: block; }

.nav-item { display: flex; align-items: center; }
.nav-links a {
  padding: var(--space-4); border-radius: var(--radius);
  color: var(--text); font-size: var(--step-1); font-weight: 600; flex: 1;
  min-height: 3rem; display: flex; align-items: center;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.nav-links a:hover { background: var(--surface-2); color: var(--text); }
.nav-links a > .icon { flex: none; margin-inline-end: var(--space-2); }

/* A parent category shows its own link plus an arrow that reveals its children,
   so a visitor can reach a subcategory without loading the category page first. */
.nav-item--parent { display: flex; flex-wrap: wrap; align-items: center; }
.nav-sub-arrow {
  display: inline-flex; align-items: center; justify-content: center;
  width: 3rem; height: 3rem; cursor: pointer; border-radius: var(--radius);
  color: var(--text-muted); flex: none;
}
.nav-sub-arrow:hover { background: var(--surface-2); color: var(--text); }
.nav-sub-toggle:focus-visible + .nav-sub-arrow { outline: none; box-shadow: var(--ring); }
.caret {
  width: 0.45em; height: 0.45em;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: rotate(45deg); margin-top: -0.2em;
  transition: transform var(--dur) var(--ease);
}
.nav-sub-toggle:checked + .nav-sub-arrow .caret { transform: rotate(-135deg); margin-top: 0.15em; }
.nav-sub {
  display: none; flex-direction: column; width: 100%; gap: var(--space-1);
  padding-inline-start: var(--space-4); padding-block-end: var(--space-2);
}
.nav-sub-toggle:checked ~ .nav-sub { display: flex; }
.nav-sub a { font-size: var(--step-0); color: var(--text-muted); font-weight: 500; min-height: 2.75rem; }

/* Profile Details keeps optional social channels available without letting four
   similar inputs dominate the form. Native details preserves the no-JS path and
   opens automatically when validation has something inside to explain. */
.form-disclosure {
  grid-column: 1 / -1;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface-2); padding: 0 var(--space-4);
}
.form-disclosure > summary {
  min-height: 3rem; display: flex; align-items: center;
  cursor: pointer; font-weight: 650; color: var(--text);
}
.form-disclosure__fields { padding-block: 0 var(--space-4); }

.lang-switch { display: inline-flex; gap: var(--space-1); align-items: center; }
.lang-switch form { display: inline; margin: 0; }
.lang-option { font: inherit; font-size: var(--step--1); font-weight: 600; cursor: pointer; padding: var(--space-1) var(--space-2); border-radius: var(--radius-pill); border: 1px solid var(--border); background: var(--surface); color: var(--text-muted); }
.lang-option:hover { background: var(--surface-2); color: var(--text); }
.lang-option--active { background: var(--accent-soft); color: var(--accent); border-color: transparent; }

@media (min-width: 720px) {
  .nav-burger, .nav-scrim { display: none; }
  .nav-toggle:checked ~ .nav-scrim { display: none; }
  .nav-links {
    position: static; display: flex; flex-direction: row; flex-wrap: wrap;
    align-items: center; justify-content: flex-end; gap: var(--space-1);
    min-width: 0; max-height: none; overflow: visible; padding: 0;
    background: none; border: 0; box-shadow: none;
  }
  .nav-links > a, .nav-item--parent > a {
    padding: var(--space-2) var(--space-3); border-radius: var(--radius-pill);
    color: var(--text-muted); font-size: var(--step--1); font-weight: 550; flex: initial;
  }
  .nav-links a:hover { color: var(--text); }
  .nav-item--cta { color: var(--accent) !important; font-weight: 650 !important; }

  /* Subcategories open on hover or keyboard focus. On desktop the arrow is part of
     the same hover target as the label — a separate control next to it contradicts
     the mental model, and clicking it fought the hover state (the caret flipped
     while the panel stayed open). Here it is a pure affordance: pointer-events are
     off so a click falls through to the category link. */
  .nav-item--parent { position: relative; flex-wrap: nowrap; align-items: center; }
  .nav-item--parent > a { display: inline-flex; align-items: center; gap: var(--space-2); }
  .nav-sub-arrow {
    position: absolute; inset-inline-end: var(--space-2); top: 50%;
    transform: translateY(-50%);
    width: auto; height: auto; pointer-events: none; background: none;
  }
  .nav-item--parent > a { padding-inline-end: var(--space-6); }
  /* Hover/focus drives the caret, not the checkbox, so the two never disagree. */
  .nav-sub-toggle:checked + .nav-sub-arrow .caret { transform: rotate(45deg); margin-top: -0.2em; }
  .nav-item--parent:hover .nav-sub-arrow .caret,
  .nav-item--parent:focus-within .nav-sub-arrow .caret { transform: rotate(-135deg); margin-top: 0.15em; }
  /* The panel floats below the bar. Without this it inherits the mobile rule
     (static, full width) and renders inside the menu bar itself. */
  .nav-sub {
    position: absolute; inset-inline-start: 0; top: calc(100% + var(--space-2));
    min-width: 13rem; width: max-content; max-width: 22rem;
    padding: var(--space-2); gap: 2px;
    background: var(--surface); border: 1px solid var(--border-strong);
    border-radius: var(--radius); box-shadow: var(--shadow);
  }
  /* Bridge the visual gap between the item and its panel. Without it the mouse
     crosses a strip that belongs to neither element, :hover is lost and the panel
     closes mid-travel. The bridge is inside .nav-sub, so hovering it keeps the
     ancestor .nav-item--parent hovered. Invisible, and never on mobile where the
     panel is in flow. */
  .nav-sub::before {
    content: ""; position: absolute; inset-inline: 0;
    bottom: 100%; height: var(--space-3);
  }
  .nav-item--parent:hover .nav-sub,
  .nav-item--parent:focus-within .nav-sub { display: flex; }
  .nav-sub a {
    padding: var(--space-2) var(--space-3); font-size: var(--step--1);
    color: var(--text); border-radius: var(--radius-sm);
  }
  .nav-sub a:hover { background: var(--surface-2); }
}

/* ---- Icons ----------------------------------------------------------------*/
.icon {
  width: 1.25em; height: 1.25em; flex: none;
  vertical-align: -0.2em; /* optical alignment with adjacent text */
}
.icon--sm { width: 1em; height: 1em; }

/* ---- Hero -----------------------------------------------------------------*/
.hero { text-align: center; padding-block: var(--space-7) var(--space-5); }
/* Directory and profile pages lead with content, not with a landing-page hero:
   the filter bar and results should be near the fold. A full hero belongs on the
   home page, where setting the scene is the job. */
.hero--compact { padding-block: var(--space-5) var(--space-4); }
.hero--compact h1 { font-size: var(--step-3); }
.hero--compact .lead { margin-top: var(--space-3); font-size: var(--step-0); }
.hero h1 { max-width: 16ch; margin-inline: auto; }
.hero .lead { max-width: 62ch; margin: var(--space-5) auto 0; }
.hero .cluster { margin-top: var(--space-6); }
/* The home hero carries editable copy, so its heading has to survive a real
   sentence. `16ch` is right for two or three words and wraps a written headline into
   a column; the home page gets room for one and keeps the lead close under it. */
.hero--home h1 { max-width: 22ch; }
.hero--home .lead { margin-block-start: var(--space-4); }

/* ---- Home shelves -----------------------------------------------------------
   A few of the best listings under each category heading. The shelf is the reason
   the home page describes the site rather than querying it, so the heading row is
   deliberately plain: the cards are the content, and a decorated heading competes
   with them. */
.shelf__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
}
.shelf__head h2 { margin: 0; font-size: var(--step-1); }
.shelf__all {
  /* `flex-shrink: 0` so a long category name wraps rather than squeezing the link
     into a vertical stack of letters. */
  flex-shrink: 0;
  font-size: var(--step--1);
  font-weight: 600;
}
.home-browse__title {
  margin: 0;
  font-size: var(--step-1);
  /* Separated by a rule rather than by space alone: after several shelves, space
     alone reads as another gap between shelves instead of as a change of subject. */
  padding-block-start: var(--space-5);
  border-block-start: 1px solid var(--border);
}

/* ---- Buttons --------------------------------------------------------------*/
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
  padding: 0.68em 1.25em; border-radius: var(--radius-pill);
  font: inherit; font-weight: 600; font-size: var(--step-0);
  border: 1px solid transparent; cursor: pointer; white-space: nowrap;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
    border-color var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.btn:active { transform: translateY(1px); }
.btn--primary { background: var(--accent); color: var(--on-accent); }
.btn--primary:hover { background: var(--accent-hover); color: var(--on-accent); }
.btn--primary:active { background: var(--accent-press); }
.btn--secondary { background: var(--surface); color: var(--text); border-color: var(--border-strong); }
.btn--secondary:hover { background: var(--surface-2); color: var(--text); }
/* The chip whose panel is open. Used by the option-detail buttons, where the pressed
   one has to stay visibly pressed after the page reloads — the panel is a URL, so
   there is no client state to show it. */
.btn--active { background: var(--surface-2); border-color: var(--accent); color: var(--accent); }
.btn--ghost { background: transparent; color: var(--accent); }
.btn--ghost:hover { background: var(--accent-soft); }
.btn--sm { padding: 0.5em 0.95em; font-size: var(--step--1); }
.btn--block { width: 100%; }

/* ---- Cards ----------------------------------------------------------------*/
.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: var(--space-5);
  box-shadow: var(--shadow-sm);
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.card--interactive:hover { transform: translateY(-3px); box-shadow: var(--shadow); border-color: var(--border-strong); }
.card > * + * { margin-top: var(--space-3); }
.card h3 { letter-spacing: -0.02em; }
.card__foot { margin-top: var(--space-4); }

/* ---- Chips, pills, badges -------------------------------------------------*/
.pill, .chip, .badge {
  display: inline-flex; align-items: center; gap: var(--space-2);
  border-radius: var(--radius-pill); font-size: var(--step--1); font-weight: 550;
  padding: 0.4em 0.85em; border: 1px solid var(--border);
  background: var(--surface); color: var(--text-muted);
}
.chip { transition: background var(--dur) var(--ease), color var(--dur) var(--ease), border-color var(--dur) var(--ease); }
a.chip:hover { background: var(--surface-2); color: var(--text); border-color: var(--border-strong); }
.badge--accent { background: var(--accent-soft); color: var(--accent); border-color: transparent; }
.badge--success { background: var(--success-soft); color: var(--success); border-color: transparent; }
.badge--danger { background: var(--danger-soft); color: var(--danger); border-color: transparent; }

/* ---- Tabs -----------------------------------------------------------------*/
/* Profile section tabs. All sections are rendered; only the selected one shows.
   No JavaScript and no extra URLs — a hidden radio drives it, and because each
   radio sits immediately before its own label and panel, the rules below need no
   per-profile ids. `order` lifts every label into a row above the panels. */
/* ---- Profile section tabs ---------------------------------------------------
   Redesigned 2026-08-04. These were small pill chips, which understated the whole
   point of the product: one profile covering several niches is the differentiator,
   and the control that moves between them should look like primary navigation
   rather than a row of filter tags.

   Still CSS-only, and the structure is load-bearing: each radio sits immediately
   before its label and panel, so `input:checked + label + panel` does the work with
   no per-profile ids and no script. That adjacency is why there is no wrapper
   element around the labels — a wrapper would break the sibling chain. The rule
   under the strip is drawn by the panel's top border instead. */
.tabs { display: flex; flex-wrap: wrap; gap: var(--space-1); align-items: flex-start; }
.tabs__radio {
  /* Hidden but focusable: clip rather than display:none, or the tabs stop working
     from the keyboard and a screen reader loses the control entirely. */
  position: absolute; width: 1px; height: 1px; margin: -1px;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}
.tabs__label {
  order: 1; cursor: pointer; scroll-snap-align: start; white-space: nowrap;
  display: inline-flex; align-items: center; gap: var(--space-2);
  min-height: 3rem; padding: 0 var(--space-4);
  border: none; border-block-end: 2px solid transparent; background: none;
  font-size: var(--step-0); font-weight: 600; color: var(--text-muted);
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.tabs__label:hover { color: var(--text); }
.tabs__radio:checked + .tabs__heading .tabs__label {
  color: var(--text); border-block-end-color: var(--accent);
}
.tabs__radio:focus-visible + .tabs__heading .tabs__label { outline: none; box-shadow: var(--ring); }
.tabs__panel {
  order: 2; flex: 1 1 100%; display: none;
  border-block-start: 1px solid var(--border);
  margin-block-start: -1px; /* meets the active tab's own underline */
  padding-block-start: var(--space-5);
}
.tabs__radio:checked + .tabs__heading + .tabs__panel { display: block; }
.tab {
  border: 1px solid var(--border-strong); border-radius: var(--radius-pill);
  padding: var(--space-2) var(--space-4); background: var(--surface);
  color: var(--text); font-weight: 600; font-size: var(--step--1);
}
.tab:hover { background: var(--surface-2); color: var(--text); }

/* ---- Forms ----------------------------------------------------------------*/
.field { display: grid; gap: var(--space-2); text-align: start; }
.label { font-size: var(--step--1); font-weight: 650; color: var(--text); }
.label .req { color: var(--danger); }
.help { font-size: var(--step--1); color: var(--text-subtle); }
/* **The bare element selectors are here on purpose.**
 *
 * Every control used to be styled by a class a form had to remember to attach, and
 * two of them forgot: `Profile.timezone` and `Offering.calendar_timezone` were plain
 * `ChoiceField`s with no widget attrs, so they rendered as raw browser dropdowns in
 * the middle of a settings list. The owner found the second one twice — *"the timezone
 * selector field is still badly styled or not styled like before"* — and noted there
 * was another like it somewhere, which there was.
 *
 * A class you must not forget is a class someone will forget. The element carries the
 * look; `.input` / `.select` / `.textarea` stay as aliases so no existing markup has
 * to change, and adding a field can no longer produce an unstyled control. */
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="image"]):not([type="color"]):not([type="hidden"]),
select, textarea,
.input, .textarea, .select {
  width: 100%; font: inherit; color: var(--text);
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); padding: 0.7em 0.85em;
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="image"]):not([type="color"]):not([type="hidden"]):focus,
select:focus, textarea:focus,
.input:focus, .textarea:focus, .select:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }

/* A control no wider than the value it holds.
   -----------------------------------------------------------------------------
   The owner: *"some other fields are so larger than the value we gonna put in or
   select from, I don't know it's a design principle of apple or we should improve
   such styles of ours."* It is not an Apple principle — iOS sizes a field to its
   content and puts it on the trailing edge of its row. A full-width box holding
   "30" reads as though it is waiting for a sentence.

   Only the types whose content length is *known* from the type itself. Anything
   that can hold a name, an address or a note keeps the full width, because there
   the box is telling the truth about how much you may write. */
input[type="number"], input[type="time"], input[type="date"] { max-inline-size: 11rem; }
input[type="datetime-local"] { max-inline-size: 16rem; }
.textarea { min-height: 7.5rem; resize: vertical; }
.select[multiple] { min-height: 8rem; padding: var(--space-2); }
.field--error .input, .field--error .textarea, .field--error .select { border-color: var(--danger); }
.field__error { font-size: var(--step--1); color: var(--danger); font-weight: 550; }
.form-grid { display: grid; gap: var(--space-4); grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr)); align-items: start; }
.form-grid > * { min-inline-size: 0; }
.field--wide { grid-column: 1 / -1; }
/* Checkbox beside its wording, not stacked under it: a bare checkbox stretched
   into a grid cell rendered adrift from the label it belongs to. `order` does the
   swap so the markup stays shared with every other field type. */
.field--check { grid-template-columns: auto minmax(0, 1fr); align-items: center; }
.field--check input[type="checkbox"] { order: 1; width: 1.1rem; height: 1.1rem; margin: 0; accent-color: var(--accent); }
.field--check .label { order: 2; font-weight: 550; }
.field--check .help, .field--check .field__error { order: 3; grid-column: 1 / -1; }

/* ---- Filter bar (dropdown facets) -----------------------------------------*/
.filter-bar {
  /* Sticks to the top so the filters stay reachable while scrolling results,
     without a modal or an interstitial step. `top: -1px` + a slightly stronger
     shadow once stuck keeps it from looking like a detached floating bar. */
  position: sticky; top: -1px; z-index: 30;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); padding: var(--space-4);
  transition: box-shadow var(--dur) var(--ease);
}
@supports (animation-timeline: view()) {
  .filter-bar { box-shadow: var(--shadow-sm); }
}
.filter-bar__fields { display: grid; gap: var(--space-3); grid-template-columns: 1fr; }
.filter-bar__actions { display: flex; gap: var(--space-2); align-items: center; }
.filter-bar__selected {
  display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center;
  margin-top: var(--space-4); padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}
.filter-note { margin-top: var(--space-3); }

/* Dropdown field: <details> styled as a form control, panel overlays content. */
.dropdown { position: relative; }
.dropdown__control {
  display: flex; align-items: baseline; gap: var(--space-2);
  width: 100%; cursor: pointer; list-style: none;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  padding: 0.7em 0.85em; font-size: var(--step-0);
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.dropdown__control::-webkit-details-marker { display: none; }
.dropdown__control:hover { border-color: var(--accent); }
.dropdown[open] > .dropdown__control { border-color: var(--accent); box-shadow: var(--ring); }
.dropdown__control:focus-visible { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
.dropdown__label { font-size: var(--step--1); font-weight: 650; color: var(--text-muted); }
.dropdown__value { font-weight: 600; flex: 1; }
.dropdown__caret {
  /* Physical borders are deliberate here: right+bottom rotated 45deg is a
     symmetric down-chevron, so it reads the same in RTL. Logical properties
     would flip it to point the wrong way. */
  width: 0.5em; height: 0.5em; align-self: center; flex: none;
  border-right: 2px solid var(--text-subtle); border-bottom: 2px solid var(--text-subtle);
  transform: rotate(45deg); margin-top: -0.25em;
}
.dropdown[open] .dropdown__caret { transform: rotate(-135deg); margin-top: 0.15em; }
/* Mobile: fields are stacked, so the panel stays in flow — an overlay would
   cover the next field and the Apply button. Desktop switches to an overlay
   below so the results grid is never pushed down. */
/* The panel itself no longer scrolls — the tree inside it does. When the panel was
   the scroll container, the "Selected" strip and the Apply button scrolled with it:
   sticky kept them on screen, but the rows slid *underneath* and the last option was
   permanently half-hidden behind the button. Three rows in a column, with only the
   middle one scrolling, is the shape that has no overlap to fix. */
.dropdown__panel {
  margin-block-start: var(--space-2);
  min-width: 100%; max-height: min(65vh, 30rem);
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: var(--space-2); display: flex; flex-direction: column; gap: 2px;
}
.picker__tree { overflow-y: auto; min-height: 0; flex: 1; }
.option {
  display: flex; align-items: center; gap: var(--space-3);
  padding: 0.5em 0.6em; border-radius: var(--radius-sm); cursor: pointer;
  font-size: var(--step--1); color: var(--text); white-space: nowrap;
}
.option input { width: 1.15em; height: 1.15em; }
.option:hover { background: var(--surface-2); }
.option input { accent-color: var(--accent); flex: none; margin: 0; }
.option__name { flex: 1; }
.option__count { color: var(--text-subtle); font-variant-numeric: tabular-nums; }
/* An ancestor of the current selection. Not checked — checking Alborz as well as
   Karaj would mean the whole province — but the branch you are inside has to look
   different from the thirty provinces you are not. */
.option--on-path .option__name { color: var(--text); font-weight: 650; }
.option--on-path { background: var(--surface-2); }

/* ---- Tree picker (expandable multi-select over a hierarchy) -----------------
   Replaced a flat indented list, where depth was one class per level and so had a
   hard ceiling — locations have five levels and the list could style three. Nesting
   the markup instead means indentation is structural: a child is inside its parent,
   at any depth, and a branch can collapse. Everything below mirrors in RTL because
   the indent is `padding-inline-start` on the nested list, not a per-level margin. */
.picker__list { list-style: none; margin: 0; padding: 0; display: grid; gap: 1px; }
.picker__children > .picker__list {
  padding-inline-start: var(--space-4);
  border-inline-start: 2px solid var(--border);
  margin-inline-start: var(--space-3);
}
.picker__summary {
  display: flex; align-items: center; gap: var(--space-1);
  list-style: none; cursor: pointer; border-radius: var(--radius-sm);
}
.picker__summary::-webkit-details-marker { display: none; }
.picker__summary:hover { background: var(--surface-2); }
.picker__summary .option { flex: 1; min-width: 0; }
.picker__summary .option:hover { background: transparent; }
.picker__summary .option__name { overflow: hidden; text-overflow: ellipsis; }

/* The twisty belongs to the disclosure, the control to the choice. Keeping them
   visibly separate is the whole reason a branch can be opened without being picked —
   but they were separate and *tiny*, so opening a province meant hitting a 7px
   chevron. The arrow still looks small; its hit area is 2.75rem square, which is the
   size a thumb actually needs. The box is the target, the chevron is just the paint. */
.picker__twisty {
  position: relative; flex: none;
  width: 2.75rem; height: 2.75rem;
  border-radius: var(--radius-sm);
}
.picker__twisty::after {
  content: ""; position: absolute; inset-block-start: 50%; inset-inline-start: 50%;
  width: 0.5em; height: 0.5em;
  margin-block-start: -0.35em; margin-inline-start: -0.25em;
  border-right: 2px solid var(--text-muted); border-bottom: 2px solid var(--text-muted);
  transform: rotate(-45deg); transform-origin: center;
  transition: transform var(--dur) var(--ease);
}
.picker__summary:hover .picker__twisty { background: var(--surface-3, var(--surface-2)); }
.picker__branch[open] > .picker__summary > .picker__twisty::after { transform: rotate(45deg); }
.picker__summary:focus-visible { outline: none; box-shadow: var(--ring); }
/* Leaves line up with their siblings' labels, not with the twisty they do not have. */
.option--leaf { margin-inline-start: 2.75rem; }
.picker__children .option--leaf { margin-inline-start: 2.75rem; }

/* A branch that contains the choice. Its own control stays unchecked — picking Karaj
   is not picking Alborz — so the mark says "in here" rather than "this". The script
   also sets `indeterminate`, which renders as a dash in the box itself. */
.option--ancestor { background: var(--accent-soft); }
.option--ancestor .option__name { color: var(--text); font-weight: 650; }

/* A parent that cannot be chosen at all: sections are leaf-only, so it is a heading
   with a disclosure, and it must not look like a control that failed to work. */
.option--container { cursor: pointer; }
.option--container .option__name {
  font-weight: 650; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: 0.03em; font-size: var(--step--2);
}

/* Rows are touch-sized. A picker is used with a thumb, in a scrolling panel, where a
   mis-tap either chooses the wrong place or collapses the branch you just opened. */
.picker__list .option { min-height: 2.75rem; }
.option__name { overflow-wrap: anywhere; white-space: normal; }

/* Chosen options, above the tree and outside its scroll area — the answer to "what
   have I picked?" must not scroll away with the thing you are picking from. */
.picker__chosen {
  flex: none;
  display: grid; gap: var(--space-2);
  padding: var(--space-2); margin-block-end: var(--space-2);
  background: var(--surface-2); border-radius: var(--radius-sm);
}
.picker__chosen[hidden] { display: none; }
.picker__chosen-label {
  font-size: var(--step--2); font-weight: 650; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: 0.04em;
}
.picker__chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.chip {
  display: inline-flex; align-items: center; gap: var(--space-1);
  padding: 0.25em 0.6em; border-radius: 999px;
  background: var(--surface); border: 1px solid var(--border-strong);
  font-size: var(--step--2); color: var(--text); line-height: 1.6;
}
.chip--removable { cursor: pointer; font: inherit; font-size: var(--step--2); }
.chip--removable:hover { border-color: var(--accent); }
.chip__x { color: var(--text-subtle); font-size: 1.15em; line-height: 1; }
.picker__actions {
  flex: none;
  padding-block-start: var(--space-2); margin-block-start: var(--space-2);
  border-top: 1px solid var(--border); background: var(--surface);
}

/* Selection tree — an already-chosen hierarchy shown in place (owner panel).
   Rows, not cards: a card per node split one nested place into several tiles that
   read as unrelated areas. Depth comes from a `--depth` custom property set on the
   row, so it scales to all five location levels without a class per level, and the
   spine plus indent use logical properties to mirror in RTL. */
.tree { list-style: none; margin: 0; padding: 0; }
.tree__row {
  display: flex; align-items: center; gap: var(--space-3);
  /* Deep rows carry a long name plus type, flag and Remove. Without wrapping,
     a long name (Persian district names run long) overlapped the type label at
     390px — the row must reflow instead. */
  flex-wrap: wrap;
  padding-block: var(--space-2);
  /* Mobile-first: a neighborhood sits four levels deep, and the wider desktop
     step would spend a quarter of a 390px screen on indent alone. */
  padding-inline-start: calc(var(--depth, 0) * var(--space-3));
  font-size: var(--step--1);
}
.tree__row + .tree__row { border-top: 1px solid var(--border); }
.tree__name { font-weight: 600; color: var(--text); min-width: 0; overflow-wrap: anywhere; }
.tree__type { color: var(--text-subtle); white-space: nowrap; }
/* Ancestors nobody picked: present for orientation, so they recede and offer no
   controls. Without them a bare "Karaj" never says which province it is in. */
.tree__row--context .tree__name { font-weight: 500; color: var(--text-muted); }
.tree__flag {
  font-size: var(--step--1); font-weight: 700; letter-spacing: 0.02em;
  text-transform: uppercase; color: var(--accent); white-space: nowrap;
}
/* Not-yet-live states are information, not achievements — they should not read
   as loud as the accent colour reserved for "this is working". */
.tree__flag--muted { color: var(--text-subtle); }
/* Something the owner still owes, not something that went wrong — so it borrows the
   danger hue without the alert's full background wash. */
/* `--warning` since 2026-08-12. This flag means "not set up yet" — it was drawing
   an unfinished calendar in the colour reserved for something being wrong. */
.tree__flag--warn { color: var(--warning); }
/* Same fix as `.identity__handle`: isolate the run, never flip the box. */
.dashboard-handle {
  margin: var(--space-2) 0 0;
  inline-size: fit-content;
  color: var(--text-muted);
  font-size: var(--step-1);
  direction: ltr; unicode-bidi: isolate;
}
.dashboard-tools { align-items: stretch; }
.dashboard-tools .btn { position: relative; }

/* The balance is the only number an owner needs before deciding what to do. It is
   set like an account-book total, while the surrounding controls keep the existing
   product rhythm instead of turning payments into a separate visual brand. */
.balance-hero { border-bottom: 1px solid var(--border); }
.balance-amount {
  margin: var(--space-3) 0 0; color: var(--text); font-size: var(--step-4);
  font-weight: 750; letter-spacing: -0.035em;
}
.balance-amount small { color: var(--text-muted); font-size: var(--step-0); letter-spacing: 0; }
.payment-layout { display: grid; gap: var(--space-5); align-items: start; }
.payment-instructions {
  padding: var(--space-4); border-inline-start: 3px solid var(--accent);
  background: var(--surface-2); border-radius: var(--radius-sm);
}
.payment-instructions p { margin: var(--space-2) 0 0; }
.amount-presets { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.amount-chip {
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface-2);
  color: var(--text);
  cursor: pointer;
  font: inherit;
  font-size: var(--step--1);
  padding: var(--space-2) var(--space-3);
}
.amount-chip:hover,
.amount-chip[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
.amount-chip:focus-visible { outline: none; box-shadow: var(--ring); }
.payment-method-options { display: grid; gap: var(--space-2); }
.payment-method-options label {
  display: flex; align-items: center; gap: var(--space-2);
  min-block-size: 2.75rem; padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface);
}
.payment-method-options label:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
.payment-list { list-style: none; margin: 0; padding: 0; }
.payment-list li {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-4);
  padding-block: var(--space-3); border-top: 1px solid var(--border);
}
.payment-list li:first-child { border-top: 0; }
.payment-list li > div { display: grid; gap: var(--space-1); min-width: 0; }
.payment-numbers { text-align: end; flex: none; }
.payment-claim-state { align-items: end; justify-items: end; flex: none; }
.money-positive { color: var(--success); }
@media (min-width: 56rem) {
  .payment-layout { grid-template-columns: minmax(18rem, 0.8fr) minmax(25rem, 1.2fr); }
  .payment-statement { position: sticky; top: var(--space-5); }
}
@media (max-width: 32rem) {
  .payment-list--statement li { align-items: start; }
  .payment-list--statement .payment-numbers { max-width: 45%; }
}
.setup-dot {
  inline-size: 0.45rem;
  block-size: 0.45rem;
  border-radius: var(--radius-pill);
  background: var(--danger);
  flex: none;
}
/* Supporting text (a section's bio excerpt). Truncates rather than pushing the
   controls off the row; on a narrow screen it wraps to its own line instead. */
/* Supporting text (a section's bio excerpt). Truncates rather than pushing the
   controls off the row. On mobile it always takes its own line — sharing the row
   left it inline on short names and wrapped on long ones, which read as broken. */
.tree__note {
  color: var(--text-muted); flex: 1 1 100%; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tree__actions { margin-inline-start: auto; flex: none; }
/* Separate root hierarchies from each other more strongly than sibling rows. */
.tree + .tree { margin-top: var(--space-4); padding-top: var(--space-4); border-top: 2px solid var(--border-strong); }

/* Profile setup checklist (owner dashboard). Shares the tree's row rhythm but is
   flat, so it gets its own names — `.tree__*` means hierarchy. Done steps recede
   to one quiet line; what is left to do is what the block is about. */
.checklist { list-style: none; margin: 0; padding: 0; }
.checklist__item {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  padding-block: var(--space-3); font-size: var(--step--1);
}
.checklist__item + .checklist__item { border-top: 1px solid var(--border); }
.checklist__mark { flex: none; font-weight: 700; color: var(--text-subtle); }
.checklist__item--done { padding-block: var(--space-2); }
.checklist__item--done .checklist__mark { color: var(--success); }
.checklist__item--done .checklist__label { color: var(--text-muted); }
.checklist__label { font-weight: 600; color: var(--text); }
/* Required is a property of the step, not a thing the button does — the button
   always says what it does. */
.checklist__required {
  flex: none; font-size: var(--step--1); font-weight: 700; letter-spacing: 0.02em;
  text-transform: uppercase; color: var(--danger);
}
/* The reason, not the instruction. Its own line on mobile, inline when there is
   room — on a wide screen three stacked lines per step read as disconnected. */
.checklist__why { color: var(--text-muted); flex: 1 1 100%; min-width: 0; }
.checklist__action { margin-inline-start: auto; flex: none; }

/* Media grids. Fixed aspect tiles so a wall of mixed portrait and landscape photos
   still reads as a grid rather than a ragged column.

   The tinted background is a deliberate placeholder: with width/height on every img
   the browser reserves the box before the bytes arrive, so a slow connection sees a
   calm grey block that becomes a photo — not a page that jumps. That is cheaper and
   steadier than a skeleton animation, which mostly advertises that something is
   slow. */
.media-grid { display: grid; gap: var(--space-4); grid-template-columns: repeat(auto-fill, minmax(min(100%, 180px), 1fr)); }
.media-tile { margin: 0; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; background: var(--surface); }
.media-tile img { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; background: var(--surface-2); }
.media-tile figcaption { padding: var(--space-3); font-size: var(--step--1); }
.media-tile--logo { border-color: var(--accent); }

/* A gallery block the owner set to "slider": one row that scrolls sideways.
   `grid-auto-flow: column` keeps the same grid and the same tiles — only the axis
   changes — so the lightbox grouping, the hover transition and the dark theme all
   carry over untouched.

   `scroll-snap` makes it stop on a picture rather than between two. RTL needs
   nothing here: a grid's inline axis already follows the document direction, which
   is exactly why the rest of this file avoids `left`/`right`. */
.media-grid--slider {
  grid-auto-flow: column;
  grid-auto-columns: minmax(min(78%, 240px), 1fr);
  grid-template-columns: none;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  overscroll-behavior-inline: contain;
  padding-block-end: var(--space-2);
}
.media-grid--slider > .media-tile { scroll-snap-align: start; }

/* Editing a gallery block: thumbnails with a grip and a remove button. Wider tiles
   than the public grid would give, because the job here is recognising which picture
   is which, not showing the page as visitors see it. */
.gallery-edit {
  display: grid; gap: var(--space-3);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 150px), 1fr));
}
.gallery-edit__tile {
  margin: 0; position: relative;
  border: 1px solid var(--border); border-radius: var(--radius);
  overflow: hidden; background: var(--surface);
}
.gallery-edit__tile img { display: block; width: 100%; aspect-ratio: 1 / 1; object-fit: cover; background: var(--surface-2); }
.gallery-edit__tools { padding: var(--space-2); justify-content: center; gap: var(--space-1); }
/* Same rule as `.tree__grip`: the handle only appears once the script has actually
   made the tile draggable, so it never advertises something that does not work. */
.gallery-edit__grip {
  display: none; position: absolute;
  inset-block-start: var(--space-2); inset-inline-start: var(--space-2);
  padding: 0 var(--space-1); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text-subtle);
  cursor: grab; letter-spacing: -2px;
}
.is-reorderable .gallery-edit__grip { display: inline; }
/* The manager sits after the panel's own form, so it needs its own separation —
   otherwise it reads as part of the Save row directly above it. */
.gallery-manager {
  padding: var(--space-4) var(--space-5) var(--space-5);
  border-block-start: 1px solid var(--hairline);
}

/* Profile logo on the public page and on result cards. */
/* `margin-inline: auto` because the global reset makes images block-level, so the
   hero's text-align does not centre them. */
/* Avatars. The slot is the same size whether or not there is a photo, so a listing
   has one layout rather than two, and a profile without a picture reads as "not yet"
   instead of "broken". Sizes are set here rather than by the image, so swapping a
   preset never changes the page. */
/* Reorderable rows. The grip only appears once the script has made them draggable,
   so it never advertises an affordance that does not work. */
.tree__grip { display: none; color: var(--text-subtle); cursor: grab; letter-spacing: -2px; }
.is-reorderable .tree__grip { display: inline; }
.is-reorderable .tree__row { background: var(--surface); }
.tree__row.is-dragging { opacity: 0.5; }
.block-image { width: 100%; border-radius: var(--radius-sm); object-fit: cover; background: var(--surface-2); }
.category-hero { width: 100%; max-height: 22rem; object-fit: cover; border-radius: var(--radius); background: var(--surface-2); }
.avatar {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--radius-sm); object-fit: cover; background: var(--surface-2);
  overflow: hidden;
}
.avatar--sm { width: 2.5rem; height: 2.5rem; }
/* No auto margin any more: the identity block is a grid and the avatar has its
   own column, so centring it here fought the layout. Larger, too — it is the
   only image in the header now that the block is not centred. */
.avatar--lg { width: 5rem; height: 5rem; border-radius: var(--radius); }
@media (min-width: 40rem) { .avatar--lg { width: 6.5rem; height: 6.5rem; } }
/* The initial, when there is no photo. Two tints so a page of them is not a grid of
   identical grey squares; both hold contrast against their text in either theme. */
.avatar--empty { font-weight: 700; color: var(--text-muted); letter-spacing: -0.02em; }
/* A fill, not a surface: the placeholder sits on cards *and* on the tinted page. */
.avatar--empty[data-tint="a"] { background: var(--fill-3); }
.avatar--empty[data-tint="b"] { background: var(--accent-soft, var(--surface-2)); }
.avatar--sm.avatar--empty { font-size: var(--step-0); }
.avatar--lg.avatar--empty { font-size: var(--step-2); display: flex; }

/* Selected-filter chips (removable) */
.facet-group__label { font-size: var(--step--1); font-weight: 700; color: var(--text-muted); }
.facet {
  display: inline-flex; align-items: center; gap: var(--space-2);
  border: 1px solid var(--border-strong); border-radius: var(--radius-pill);
  padding: 0.4em 0.85em; font-size: var(--step--1); font-weight: 600;
  color: var(--text); background: var(--surface);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.facet:hover { background: var(--surface-2); color: var(--text); border-color: var(--border-strong); }
.facet--active { background: var(--accent); color: var(--on-accent); border-color: transparent; }
.facet--active:hover { background: var(--accent-hover); color: var(--on-accent); }
.facet__x { font-weight: 700; margin-inline-start: var(--space-1); }

@media (min-width: 720px) {
  .filter-bar__fields {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
    gap: var(--space-4); align-items: center;
  }
  .dropdown__panel {
    position: absolute; z-index: 20;
    inset-inline-start: 0; inset-block-start: calc(100% + var(--space-2));
    margin-block-start: 0;
  }
  /* Room to spare here, so give the hierarchy a clearer step than on mobile. */
  .tree__row { padding-inline-start: calc(var(--depth, 0) * var(--space-5)); }
  /* Wide enough for the note to share the row with the name and controls. */
  .tree__note { flex: 1 1 12rem; }
  .checklist__why { flex: 1 1 14rem; }
}

/* ---- Category tiles (home) ------------------------------------------------*/
.tiles { display: grid; gap: var(--space-3); grid-template-columns: repeat(auto-fill, minmax(min(100%, 200px), 1fr)); }
.tile {
  display: flex; flex-direction: column; gap: var(--space-1);
  min-height: 4.5rem; justify-content: center;
  padding: var(--space-4) var(--space-5);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); color: var(--text);
  transition: border-color var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.tile:hover { border-color: var(--accent); transform: translateY(-2px); color: var(--text); }
.tile__name { font-weight: 650; font-size: var(--step-0); }
.tile__meta { font-size: var(--step--1); color: var(--text-subtle); }

/* ---- Breadcrumb -----------------------------------------------------------*/
.breadcrumb { padding-block-start: var(--space-5); font-size: var(--step--1); }
.breadcrumb ol { list-style: none; display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin: 0; padding: 0; }
.breadcrumb li { display: inline-flex; align-items: center; gap: var(--space-2); color: var(--text-subtle); }
/* Separator is a slash rather than an arrow: direction-neutral, so it needs no RTL
   variant. Generated content is decorative and hidden from assistive tech. */
.breadcrumb li + li::before { content: "/"; color: var(--border-strong); }
.breadcrumb a { color: var(--text-muted); }
.breadcrumb a:hover { color: var(--text); text-decoration: underline; }
.breadcrumb [aria-current="page"] { color: var(--text); font-weight: 550; }

/* ---- Directory results -----------------------------------------------------*/
.results-bar {
  display: flex; flex-wrap: wrap; gap: var(--space-3);
  align-items: baseline; justify-content: space-between;
}
.results-count { color: var(--text-muted); font-size: var(--step--1); margin: 0; }
.sort { display: flex; flex-wrap: wrap; gap: var(--space-1); align-items: center; }
.sort__label { font-size: var(--step--1); color: var(--text-subtle); margin-inline-end: var(--space-1); }
.sort__option {
  font-size: var(--step--1); font-weight: 550; color: var(--text-muted);
  padding: var(--space-1) var(--space-3); border-radius: var(--radius-pill);
}
.sort__option:hover { background: var(--surface-2); color: var(--text); }
.sort__option--active { background: var(--accent-soft); color: var(--accent); font-weight: 650; }

.result-meta { display: flex; flex-wrap: wrap; gap: var(--space-4); align-items: center; color: var(--text-subtle); font-size: var(--step--1); }
.meta-item { display: inline-flex; align-items: center; gap: var(--space-2); }
.result-meta .dot { width: 3px; height: 3px; border-radius: 50%; background: currentColor; opacity: 0.6; }

/* ---- Alerts / notices -----------------------------------------------------*/
.alert {
  display: flex; gap: var(--space-3); align-items: flex-start;
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: var(--space-4); background: var(--surface); color: var(--text-muted);
}
.alert--success { background: var(--success-soft); border-color: transparent; color: var(--success); }
.alert--info { background: var(--accent-soft); border-color: transparent; color: var(--accent); }
.alert--danger, .alert--error { background: var(--danger-soft); border-color: transparent; color: var(--danger); }
/* On `--surface`, not `--surface-2`. A notice is a callout *on* the page, and once
   the page became tinted (2026-08-12) a `#f5f5f7` box on a `#f2f2f7` page was
   invisible — the exact failure the tint was introduced to fix, reproduced one layer
   up. The rule this is an instance of: anything meant to sit on the page uses
   `--surface`; `--surface-2` is a state *on* a surface, not a surface itself. */
.notice { border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-4); background: var(--surface); color: var(--text-muted); }
/* A notice that is a debt, not an FYI. Pale red rather than the full alert wash:
 * it sits on the owner's own profile for as long as the job is undone, and an
 * unmissable banner that never goes away stops being read within a day. The tint
 * is enough to mark it as the one thing on the page still owed. */
.notice--danger { border-color: var(--danger); background: var(--danger-soft); color: var(--danger); }
.notice--danger .help { color: inherit; opacity: 0.85; }
.notice__body { display: grid; gap: var(--space-1); flex: 1 1 auto; }
.notice__body .help { margin: 0; }

/* ---- Footer ---------------------------------------------------------------*/
.footer { border-top: 1px solid var(--border); margin-top: var(--space-9); padding-block: var(--space-6); color: var(--text-subtle); font-size: var(--step--1); }
.footer-inner { display: flex; flex-wrap: wrap; gap: var(--space-4); justify-content: space-between; align-items: center; }

/* ---- Utilities ------------------------------------------------------------*/
.text-center { text-align: center; }
.divider { height: 1px; background: var(--border); border: 0; margin-block: var(--space-6); }

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

/* Shared confirmation dialog. Small, centred, and phrased by the product rather than
   by the browser — window.confirm cannot be styled and, on some mobile browsers,
   offers to suppress every later dialog. */
.confirm {
  border: none; border-radius: var(--radius); padding: 0;
  max-width: min(92vw, 26rem); background: var(--surface); color: var(--text);
  box-shadow: var(--shadow);
}
.confirm::backdrop { background: rgba(0, 0, 0, 0.5); }
.confirm__body { display: grid; gap: var(--space-4); padding: var(--space-5); }
.confirm__message { margin: 0; font-weight: 550; }
.btn--danger { background: var(--danger); color: #fff; border-color: transparent; }
.btn--danger:hover { filter: brightness(0.94); }

/* Reviews. Stars are text, not images: they scale with the type, need no request,
   and read correctly to a screen reader through the label beside them. */
.rating { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--step--1); }
.rating__stars { color: var(--accent); letter-spacing: 1px; }
.reviews { display: grid; gap: var(--space-3); margin-top: var(--space-5); padding-top: var(--space-5); border-top: 1px solid var(--border); }
.review { padding-block: var(--space-3); border-top: 1px solid var(--border); }
/* The reviewer's own review, shown back to them rather than as a filled form. */
.review--mine { background: var(--surface-2); border-radius: var(--radius); padding: var(--space-4); border-top: none; }
.review__edit summary { cursor: pointer; }
.review + .review { margin-top: 0; }
.review__reply {
  margin-top: var(--space-3); padding: var(--space-3);
  border-inline-start: 3px solid var(--border-strong); background: var(--surface-2);
  border-radius: var(--radius-sm); font-size: var(--step--1);
}
.rating__choice { display: inline-flex; align-items: center; gap: var(--space-1); font-size: var(--step--1); cursor: pointer; }
.rating__choice input { accent-color: var(--accent); }

/* ---- Authentication ---------------------------------------------------------
   Signing in is the one screen where someone types something half-remembered, on a
   phone, often in a hurry, and where failing means not becoming a user at all. It
   gets a narrower column, larger controls and more space than a settings form.

   The alternative route sits *outside* the card, as a full-width line rather than a
   footnote under the button. It used to be small ghost text below the form, which is
   how someone who registered by phone ended up staring at a password field. */
.auth { max-width: 26rem; margin-inline: auto; }
.auth__head { text-align: center; margin-block-end: var(--space-5); }
.auth__head h1 { margin-block: var(--space-2) 0; }
.auth__head .lead { margin-block-start: var(--space-3); font-size: var(--step--1); }
.auth__card { gap: var(--space-5); }
.auth__card .input {
  /* 16px minimum: anything smaller makes iOS Safari zoom the page on focus, which
     scrolls the field out of view at the exact moment of typing into it. */
  font-size: max(1rem, var(--step-0));
  padding-block: 0.85em;
}
.auth__alt {
  margin-block-start: var(--space-5);
  text-align: center;
  font-size: var(--step--1);
  color: var(--text-muted);
}
.auth__alt a { font-weight: 600; }

/* "Did not get it? Send it again" — one line, centred under the card, sitting at
   the weight of a footnote rather than competing with the primary button above it.
   It is its own <form>: forms do not nest, and a second submit inside the verify
   form would let an autofilled code plus Enter send another SMS. */
.auth__resend {
  margin-block-start: var(--space-4);
  display: flex;
  justify-content: center;
  align-items: baseline;
  gap: var(--space-2);
  font-size: var(--step--1);
  color: var(--text-muted);
}
.auth__resend button[disabled] {
  /* Disabled here means "already sent, wait" rather than "not available", so it
     stays legible instead of fading out of the conversation. */
  opacity: 0.65;
  cursor: default;
}

/* ---- Switch -----------------------------------------------------------------
   `is_active` was a bare checkbox labelled "Is active", which reads as a form field
   to fill in rather than a thing that is currently on. A switch says which state you
   are in *now* and that flipping it takes effect — the same information the checkbox
   technically carried and nobody read.

   Still a real `<input type="checkbox">` underneath: it submits, it focuses, screen
   readers announce it, and with CSS off it degrades to a checkbox rather than
   vanishing. Only the paint changes. */
.switch {
  display: flex; align-items: center; gap: var(--space-3);
  cursor: pointer; min-height: 2.75rem;
}
.switch input {
  appearance: none; -webkit-appearance: none;
  flex: none; margin: 0; cursor: pointer;
  width: 2.75rem; height: 1.6rem; border-radius: 999px;
  background: var(--border-strong); position: relative;
  transition: background var(--dur) var(--ease);
}
.switch input::after {
  content: ""; position: absolute; inset-block-start: 0.2rem; inset-inline-start: 0.2rem;
  width: 1.2rem; height: 1.2rem; border-radius: 50%;
  background: var(--surface); box-shadow: var(--shadow-sm);
  transition: transform var(--dur) var(--ease);
}
.switch input:checked { background: var(--accent); }
/* Logical translation, so the knob travels the correct way in RTL. */
.switch input:checked::after { transform: translateX(1.15rem); }
[dir="rtl"] .switch input:checked::after { transform: translateX(-1.15rem); }
.switch input:focus-visible { outline: none; box-shadow: var(--ring); }
.switch__text { font-weight: 550; }
.switch__state { color: var(--text-subtle); font-size: var(--step--1); }

/* A row whose thing is switched off. Legible, clearly secondary, never invisible —
   an owner has to be able to find what they turned off in order to turn it back on. */
.is-off { opacity: 0.55; }
.is-off .tree__name { text-decoration: line-through; text-decoration-thickness: 1px; }

/* ---- Owner section rows -----------------------------------------------------
   Name, status, path and bio were one line, so the most important thing on the
   screen — which section is this — had to be picked out of a sentence. Two lines:
   what it is, then everything about it. */
.tree__row--rich {
  display: grid;
  grid-template-columns: auto 1fr auto;
  grid-template-areas: "grip name actions" ". meta actions";
  gap: var(--space-2) var(--space-3);
  align-items: center;
  padding-block: var(--space-4);
}
.tree__row--rich .tree__grip { grid-area: grip; font-size: var(--step-1); }
.tree__row--rich .tree__head { grid-area: name; display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.tree__row--rich .tree__meta { grid-area: meta; display: grid; gap: var(--space-1); min-width: 0; }
.tree__row--rich .tree__actions { grid-area: actions; }
.tree__row--rich .tree__name { font-size: var(--step-0); font-weight: 650; }
.tree__row--rich .tree__path { font-size: var(--step--2); color: var(--text-subtle); }
.tree__row--rich .tree__note { font-size: var(--step--1); color: var(--text-muted); white-space: normal; }
@media (max-width: 559px) {
  /* On a phone the controls get their own row rather than squeezing the name. */
  .tree__row--rich {
    grid-template-columns: auto 1fr;
    grid-template-areas: "grip name" ". meta" ". actions";
  }
}
/* The grip only appears once the script has made rows draggable, so it never
   advertises an affordance that does not work. Sized for a thumb. */
.is-reorderable .tree__row--rich .tree__grip {
  display: flex; align-items: center; justify-content: center;
  width: 2.5rem; height: 2.5rem; border-radius: var(--radius-sm);
}
.is-reorderable .tree__row--rich .tree__grip:hover { background: var(--surface-2); }
/* A switch spans the form grid: it is a statement about the whole thing being
   edited, not one cell of a two-column layout. */
.field--switch { grid-column: 1 / -1; }

/* The same switch as a submit button, for toggling a row in place. `role="switch"`
   with `aria-checked` is the accessible equivalent of the checkbox version, and it
   needs no JavaScript: pressing it posts and the row comes back in its new state. */
.switch__button {
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: none; padding: var(--space-2); cursor: pointer;
  min-width: 2.75rem; min-height: 2.75rem; border-radius: var(--radius-sm);
}
.switch__button:hover { background: var(--surface-2); }
.switch__button:focus-visible { outline: none; box-shadow: var(--ring); }
.switch__track {
  width: 2.25rem; height: 1.3rem; border-radius: 999px;
  background: var(--border-strong); position: relative;
  transition: background var(--dur) var(--ease);
}
.switch__track::after {
  content: ""; position: absolute; inset-block-start: 0.15rem; inset-inline-start: 0.15rem;
  width: 1rem; height: 1rem; border-radius: 50%;
  background: var(--surface); box-shadow: var(--shadow-sm);
  transition: transform var(--dur) var(--ease);
}
[role="switch"][aria-checked="true"] .switch__track { background: var(--accent); }
[role="switch"][aria-checked="true"] .switch__track::after { transform: translateX(0.95rem); }
[dir="rtl"] [role="switch"][aria-checked="true"] .switch__track::after { transform: translateX(-0.95rem); }

/* Text for screen readers only. The row toggle's meaning is carried by position and
   colour for sighted users; someone using a screen reader needs the words. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}
/* The switch's words stack: what it is, then what turning it off does. Inline they
   ran into one sentence — "Show in the directory Turn off to hide it without
   deleting anything" — which reads as a single confused instruction. */
.switch__body { display: grid; gap: var(--space-1); min-width: 0; }
.switch__body .help { margin: 0; }

/* ---- Link cards -------------------------------------------------------------
   The parts of one section, reachable from the section. Content and services are
   per subcategory but lived only under their own dashboard entries, so editing a
   section meant going back out to a list and finding the same subcategory again. */
.linkcards { display: grid; gap: var(--space-3); grid-template-columns: 1fr; }
@media (min-width: 560px) { .linkcards { grid-template-columns: repeat(3, 1fr); } }
.linkcard {
  display: grid; gap: var(--space-1); align-content: start;
  padding: var(--space-4); min-height: 4.5rem;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); color: inherit; text-decoration: none;
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.linkcard:hover { border-color: var(--accent); box-shadow: var(--shadow-sm); }
.linkcard__title { font-weight: 650; color: var(--accent); }
.linkcard__note { font-size: var(--step--1); color: var(--text-subtle); }

/* ---- Test-mode code ---------------------------------------------------------
   Deliberately loud and deliberately ugly. The danger of a mode that shows one-time
   codes on screen is not that it exists — it is that somebody forgets it is on. A
   page that announces its own insecurity gets noticed; a quiet admin setting does
   not. This is meant to be the first thing you want to remove. */
.test-code {
  border: 2px dashed var(--danger);
  border-radius: var(--radius);
  padding: var(--space-4);
  background: var(--danger-soft);
  display: grid; gap: var(--space-2);
}
.test-code__badge {
  justify-self: start;
  background: var(--danger); color: #fff;
  font-size: var(--step--2); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em;
  padding: 0.15em 0.6em; border-radius: 999px;
}
.test-code__body { margin: 0; }
.test-code__value {
  font-size: var(--step-2); font-variant-numeric: tabular-nums;
  letter-spacing: 0.15em; margin-inline-start: 0.35em;
  user-select: all;  /* one tap selects the whole code */
}
.test-code__warning { margin: 0; font-size: var(--step--1); color: var(--danger); }
/* A heading inside a form, separating the section's own fields from the ones its
   subcategory declares. Spans the grid so it reads as a divider, not a label. */
.form-section-title {
  grid-column: 1 / -1;
  margin-block: var(--space-4) 0;
  font-size: var(--step-0);
  padding-block-start: var(--space-4);
  border-top: 1px solid var(--border);
}

/* ---- Specs — a subcategory's declared fields, answered -----------------------
   These replaced feature chips. A chip could only say a field was present; these
   carry the answer, which is what a visitor actually compares profiles on. */
.spec {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-3); padding-block: var(--space-2);
  border-bottom: 1px solid var(--border);
}
.spec:last-child { border-bottom: 0; }
.spec__name { color: var(--text-muted); font-size: var(--step--1); }
.spec__value { font-weight: 600; text-align: end; }
/* Specs stack. They briefly inherited the chip row's wrapping flex and rendered
   side by side, so a name and its answer could end up on different lines from each
   other while two different specs sat together. */
.specs { list-style: none; margin: 0; padding: 0; display: grid; }
/* Inside an offering card the same rows are a menu line rather than a comparison
   table — several of them stack in one section, so they are quieter and tighter and
   separated from the facts above instead of being boxed like the section's own table. */
.specs--compact {
  margin-block-start: var(--space-2);
  padding-block-start: var(--space-2);
  border-top: 1px solid var(--border);
}
.specs--compact .spec {
  padding-block: var(--space-1);
  border-bottom: 0;
  gap: var(--space-2);
}
.specs--compact .spec__name { font-size: var(--step--2); }
.specs--compact .spec__value { font-size: var(--step--1); font-weight: 650; }

/* ---- Feature filters --------------------------------------------------------
   A subcategory's own fields as filters. Plain controls, not the tree picker: a
   price is a range and a level is a short list, and the picker's whole job is depth
   that these do not have. */
.ffilter { display: grid; gap: var(--space-2); padding: var(--space-3) var(--space-2); }
.ffilter + .ffilter { border-top: 1px solid var(--border); }
.ffilter__label { font-size: var(--step--1); font-weight: 650; color: var(--text-muted); }
.ffilter__range { display: flex; align-items: center; gap: var(--space-2); }
.ffilter__range .input { flex: 1; min-width: 0; }
.ffilter__dash { color: var(--text-subtle); flex: none; }
.input--sm, .select--sm { padding-block: 0.5em; font-size: var(--step--1); }

/* ---- Profile card -----------------------------------------------------------
   What someone chooses between two listings on: who, where, whether it can be
   booked, what it scored, and the one or two numbers its subcategory says matter.
   The rating and the specs were already in the database and shown nowhere. */
.pcard { display: grid; gap: var(--space-3); align-content: start; }
.pcard__head { display: grid; grid-template-columns: auto 1fr auto; gap: var(--space-3); align-items: center; }
.pcard__title { min-width: 0; }
.pcard__title h3 { margin: 0; font-size: var(--step-0); overflow-wrap: anywhere; }
.pcard__niche { font-size: var(--step--2); color: var(--text-subtle); }
.pcard__snippet { margin: 0; font-size: var(--step--1); }

/* A score without its count is unreadable — 5.0 from one review is not 4.6 from
   ninety — so the two never appear apart. */
.rating-badge {
  display: inline-flex; align-items: center; gap: 0.25em; flex: none;
  font-size: var(--step--1); font-weight: 650;
}
.rating-badge .icon { color: var(--warning, #e8a33d); }
.rating-badge__count { color: var(--text-subtle); font-weight: 500; font-size: var(--step--2); }

/* The offering that satisfied the filters, named above its own specs. Accent-tinted
   because it is an answer to what the visitor asked, not another line of description —
   and the specs under it describe *it*, not the whole niche. */
.pcard__match {
  margin: 0; font-size: var(--step--1); font-weight: 650;
  color: var(--accent); overflow-wrap: anywhere;
}

/* Two specs, laid out as label-then-value pairs so a price reads as a price. */
.pcard__specs { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); }
.pcard__spec { font-size: var(--step--1); font-weight: 600; }
.pcard__spec-name { color: var(--text-subtle); font-weight: 500; }

/* ---- Contact box ------------------------------------------------------------
   The three things a visitor came to do, under the name, as equal targets. They
   used to be scattered: the phone at the foot of the page, the locations in a card
   below that, the booking form inside a tab. */
.contact-box {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  gap: var(--space-2); margin-block-end: var(--space-4);
}
.contact-box__action {
  display: grid; justify-items: center; gap: var(--space-1);
  padding: var(--space-3) var(--space-2); min-height: 3.5rem;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); color: var(--accent);
  font-size: var(--step--2); font-weight: 600; text-decoration: none;
  cursor: pointer; font-family: inherit;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.contact-box__action:hover { border-color: var(--accent); background: var(--accent-soft); }
.contact-box__action:focus-visible { outline: none; box-shadow: var(--ring); }
.contact-box__action .icon { width: 1.35em; height: 1.35em; }

/* A dialog used as a phone-style sheet: full width at the bottom on small screens,
   a centred card on larger ones. */
.sheet::backdrop { background: rgba(0, 0, 0, 0.45); }
.sheet__body { display: grid; gap: var(--space-3); padding: var(--space-5); }
@media (min-width: 560px) {
  .sheet { margin-block: auto; border-radius: var(--radius); }
}

/* ---- Offerings with places --------------------------------------------------
   A class that is full still shows: a card that vanishes when it fills reads as a
   page that lost something, and someone who came back for it needs to be told. */
.offering__state { margin: 0; font-size: var(--step--1); font-weight: 650; }
.offering__state--open { color: var(--success, #1c6b3c); }
.offering__state--full { color: var(--text-subtle); }
.offering--closed { opacity: 0.72; }
/* A sheet holding a long form scrolls inside itself rather than growing past the
   viewport, so the submit button is always reachable. */
/* The amount to pay, on the request page. Big, because it is the one number the
   person is there to read, and tabular so digits line up in either script. */
.pay-amount {
  font-size: var(--step-3); font-weight: 700; margin: 0;
  font-variant-numeric: tabular-nums;
}

/* ---- Services: the accordion and its menu -----------------------------------
   Closed, a service is its name, its cheapest price and a button. Open, it is the
   menu — the options the owner arranged, each with its own price and details.

   The grid is what puts the button on the same line as the name while keeping it a
   sibling of the <details> rather than a child of the <summary>: a button inside a
   summary is a nested interactive control, announced wrong and toggling the panel
   when clicked. */
.offerings { display: grid; gap: var(--space-3); }
.offering {
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface);
}
.offering--closed { opacity: 0.7; }
.offering__row {
  display: grid; grid-template-columns: 1fr auto; align-items: start;
  gap: var(--space-3); padding: var(--space-4);
}
/* The two things `.offering__row` ever places in its first column: the disclosure, or
   the static block that stands in for it. Both must be a *single* grid child — see
   the note in `_offering.html`. */
.offering__disclosure, .offering__static { min-width: 0; }
.offering__summary {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--space-2) var(--space-3);
  cursor: pointer;
  /* The whole row is the target, not just the words — this is a phone-first
     control and a 14px text hit area is the classic mobile miss. */
  padding-block: var(--space-1);
  font-size: var(--step-0); font-weight: 650;
}
.offering__summary--static { cursor: auto; }
.offering__summary::-webkit-details-marker { display: none; }
.offering__summary::marker { content: ""; }
.offering__summary::after {
  /* **Physical borders, deliberately.** This was `border-inline-end` +
     `border-block-end`, on the reasoning that a chevron should flip with the
     document — and that is exactly what made it wrong. In RTL the inline-end border
     is the *left* one, so the corner the arrow points from moved, and the same
     rotation that reads "down" in English read as pointing inward in Persian. The
     owner: *"the arrow for menu options isn't rotating correctly, it points inward
     when the menu is open."*

     A disclosure chevron does not point along the text direction. It points **down**
     when closed and **up** when open, and down and up do not flip in RTL. Physical
     borders give that in both languages with one rule.

     No `margin-inline-start: auto`: it sat hard against the action button, which is
     the owner's other note. It follows the last fact on the row — the price, or the
     places left — where it reads as belonging to the service rather than to the
     button beside it. */
  content: "";
  inline-size: 0.45em; block-size: 0.45em;
  border-right: 2px solid var(--text-subtle); border-bottom: 2px solid var(--text-subtle);
  transform: rotate(45deg) translate(-0.1em, -0.1em);
  transition: transform var(--transition, 150ms) ease;
  align-self: center;
}
.offering__summary--static::after { content: none; }
.offering__disclosure[open] > .offering__summary::after { transform: rotate(-135deg) translate(-0.1em, -0.1em); }
.offering__name { min-width: 0; overflow-wrap: anywhere; }
.offering__facts { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-3); align-items: baseline; }
.offering__price { font-weight: 700; font-variant-numeric: tabular-nums; }
.offering__fact { font-size: var(--step--2); color: var(--text-subtle); font-weight: 400; }
.offering__fact--open { color: var(--success); font-weight: 650; }
.offering__fact--off { color: var(--text-subtle); font-weight: 650; }
.offering__note {
  font-size: var(--step--1); color: var(--text-muted);
  margin-block: var(--space-2) 0; font-weight: 400;
}
.offering__action { flex: none; }

.menu { list-style: none; margin-block: var(--space-3) 0; padding: 0; display: grid; gap: var(--space-3); }
.menu__row {
  display: grid; gap: var(--space-2);
  padding-block-start: var(--space-3);
  border-block-start: 1px solid var(--border-subtle, var(--border));
}
.menu__head {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2) var(--space-3);
}
.menu__name { font-weight: 650; font-size: var(--step--1); min-width: 0; overflow-wrap: anywhere; }
.menu__fact { font-size: var(--step--2); color: var(--text-subtle); }
/* Pushed to the far edge so a column of prices lines up down the menu, which is how
   a printed menu is read. Tabular figures keep the digits in step. */
.menu__price { margin-inline-start: auto; font-weight: 700; font-variant-numeric: tabular-nums; }

/* The answers under a menu row, set as one quiet run rather than a stack of rows.
   See the note in `_offering.html`: the stacked form is the section's spec *table*,
   and reusing it here turned a four-line menu into a sixteen-line one. */
.menu__specs {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-3);
  font-size: var(--step--2);
}
.menu__specs .spec {
  display: inline-flex; align-items: baseline; gap: var(--space-1);
  padding: 0; border: 0; justify-content: flex-start;
}
/* A separator between answers, not after the last one — `+` gives that for free and
   costs nothing when the list wraps, because the dot travels with its own item. */
.menu__specs .spec + .spec::before {
  content: "·"; color: var(--text-faint); margin-inline-end: var(--space-2);
}
.menu__specs .spec__name { color: var(--text-faint); font-size: inherit; }
.menu__specs .spec__name::after { content: ":"; }
.menu__specs .spec__value {
  color: var(--text-muted); font-weight: 600; font-size: inherit; text-align: start;
}
/* A picture will not sit on a text line. It takes the row, and drops its separator
   because it is no longer part of the run. */
.menu__specs .spec--media { flex-basis: 100%; flex-wrap: wrap; }
.menu__specs .spec--media::before { content: none; }
.spec__image { max-inline-size: 6rem; border-radius: var(--radius-sm, 4px); block-size: auto; }

@media (max-width: 34rem) {
  /* The button goes full width under the name rather than squeezing the title into
     a few characters. */
  .offering__row { grid-template-columns: 1fr; }
  .offering__action { inline-size: 100%; }
}

/* The sheet rises from the bottom on a phone, the way an app's does — it was
   appearing centred, which reads as a desktop dialog dropped onto a phone. */
.sheet {
  position: fixed; inset: auto 0 0 0;
  width: 100%; max-width: 34rem; margin-inline: auto;
  border: 0; padding: 0;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  background: var(--surface); color: var(--text);
  box-shadow: var(--shadow-lg);
  animation: sheet-rise 240ms var(--ease);
}
@keyframes sheet-rise { from { transform: translateY(100%); } to { transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) { .sheet { animation: none; } }
/* `display` ONLY on an open dialog.
   A closed <dialog> is hidden by the browser's own `display: none`. Any rule that
   sets `display` on `.sheet` unconditionally beats it, and the sheet then sits on
   the page as an interstitial from the moment it loads — which is exactly what
   happened, and is the same mistake as the `[hidden]` + `display: flex` one in the
   image widget. `tests/test_css_contracts.py` now refuses both. */
.sheet[open] { display: block; }
.sheet--tall[open] { display: flex; flex-direction: column; max-height: 88vh; }
.sheet--tall .sheet__body { overflow-y: auto; }
@media (min-width: 560px) {
  /* Desktop has room for a centred card, and a strip along the bottom of a wide
     screen reads as a cookie banner. */
  .sheet { inset: 0; margin: auto; border-radius: var(--radius); animation: none; }
}

/* ---- The service builder ----------------------------------------------------
   Three containers on one page, replacing a fifteen-field form and three screens
   an owner moved between to set one service up. Each box is a step with a number,
   a switch and a list; the "+ Add" at the foot is the only way in, so there is one
   obvious next action per container rather than a grid of fields.
   See docs/product/service-builder.md. */
/* `.builder`, `.builder__box`, `.builder__head`, `.builder__title` and
   `.builder__add` were removed on 2026-08-12. The containers are `.builder-step`
   plus `.group` now — the same card as the rest of the site — and the page is a
   `.pane`. Only the step badge survives, because the numbering was right and the
   calendar screen counts in the same voice. See the eleventh-pass layer at the foot
   of this file and `docs/design/interface-patterns.md`. */
.builder__step {
  display: grid; place-items: center;
  inline-size: 1.75rem; block-size: 1.75rem; border-radius: 50%;
  background: var(--accent); color: #fff;
  font-size: var(--step--1); font-weight: 700; font-variant-numeric: tabular-nums;
}

/* ---- Panels: a modal that is a URL ------------------------------------------
   `open` is set in the markup so the panel still renders with JavaScript off — as
   a block at the foot of the page rather than an overlay. These rules only apply
   the overlay treatment when the browser actually put it in the top layer. */
.panel {
  border: 1px solid var(--border); border-radius: var(--radius-lg, 16px);
  background: var(--surface); color: var(--text);
  padding: 0; max-inline-size: 40rem; inline-size: 100%;
  margin-block: var(--space-5); margin-inline: auto;
}
.panel--narrow { max-inline-size: 30rem; }
.panel--calendar { max-inline-size: min(72rem, calc(100vw - 2 * var(--gutter))); }
.panel--calendar:modal { max-block-size: 94vh; }
.panel--calendar .panel__head { position: static; }
.panel:modal { max-block-size: 88vh; overflow: auto; }
/* A sheet scrolls down, never sideways. `auto-fit` in `.form-grid` will happily lay
 * two `datetime-local` controls side by side and then overflow, because a native
 * date-time input has an intrinsic minimum the grid's `minmax` cannot shrink past.
 * A panel is the one place that costs real damage — the horizontal scrollbar sits on
 * the dialog box, and dragging it used to read as a backdrop click and close the
 * sheet. `dialog.js` no longer closes on that, but the better fix is for the bar not
 * to exist: stack the fields instead. */
.panel:not(.panel--calendar) .form-grid { grid-template-columns: minmax(0, 1fr); }
.panel input, .panel select, .panel textarea { max-inline-size: 100%; }
.panel::backdrop { background: rgb(0 0 0 / 0.45); }
.panel__form { display: contents; }
.panel__head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-block-end: 1px solid var(--border);
  /* Sticky so the title and the close stay reachable in a long panel — the option
     panel grows with every declared field its subcategory has. */
  position: sticky; inset-block-start: 0; background: var(--surface); z-index: 1;
}
.panel__title { margin: 0; font-size: var(--step-0); }
.panel__body { padding: var(--space-5); }
.panel__foot {
  padding: var(--space-4) var(--space-5);
  border-block-start: 1px solid var(--border);
  position: sticky; inset-block-end: 0; background: var(--surface);
}
.panel__legend {
  font-size: var(--step--2); font-weight: 650; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--text-subtle); margin: 0;
}
/* What was just added, at the top of the panel it was added from — the owner
   entering four variations watches the list grow without closing anything. */
.panel__added { list-style: none; margin: 0; padding: var(--space-3) var(--space-5) 0; display: grid; gap: var(--space-2); }
.panel__added li { display: flex; align-items: baseline; gap: var(--space-3); font-size: var(--step--1); }
.panel__added-name { font-weight: 650; }
.panel__added-price { margin-inline-start: auto; font-variant-numeric: tabular-nums; color: var(--text-subtle); }

.catalogue { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2); }
.catalogue__item {
  display: grid; gap: 2px; padding: var(--space-3) var(--space-4);
  border: 1px solid var(--border); border-radius: var(--radius);
  color: var(--text); text-decoration: none;
}
.catalogue__item:hover { background: var(--surface-2); border-color: var(--accent); }
.catalogue__label { font-weight: 650; }
.catalogue__hint { font-size: var(--step--2); color: var(--text-subtle); }

/* Shown after submission, not in the form — so it sits apart from the rows that
   can be dragged, and the separation is the explanation. */
.tree__row--after { border-block-start: 2px dashed var(--border); }

/* ---- Editing the profile from the profile page -------------------------------
   The page renders exactly as a visitor sees it, plus handles on what exists and
   dashed boxes where something could. Owner-only, edit mode only — a visitor never
   receives any of this. See docs/product/editing-in-place.md. */
/* ---- Editing controls ------------------------------------------------------
   Rebuilt 2026-08-04. Three complaints from the owner, each a specific fix:

   1. The controls were small. 1.9rem is below every touch-target guideline; they
      are now 2.5rem, which is the smallest thing a thumb hits reliably.
   2. The icons were text characters (✎ ↑ ↓ ◉ ◎ ✕), which render at different
      weights on every platform — and the show/hide pair was genuinely unreadable.
      They are now sprite icons; see templates/_icons.html.
   3. Every bar looked identical, so it was never obvious which one acted on what.
      A bar is now a labelled header physically attached to the top of its subject:
      shared border radius, no gap, and the subject carries a matching outline. */
.edit-tools {
  display: flex; align-items: center; gap: var(--space-1);
  flex-wrap: wrap;
}
.edit-tools__grip { cursor: grab; color: var(--text-subtle); display: grid; place-items: center; }
.edit-tools__btn {
  display: grid; place-items: center;
  inline-size: 2.5rem; block-size: 2.5rem;
  border: 1px solid var(--border); border-radius: 50%;
  background: var(--surface); color: var(--accent);
  font-size: var(--step--1); line-height: 1; cursor: pointer; text-decoration: none;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.edit-tools__btn:hover { background: var(--surface-2); border-color: var(--accent); }
.edit-tools__btn:focus-visible { outline: none; box-shadow: var(--ring); }
.edit-tools__btn--danger { color: var(--danger, #c0392b); }

/* The bar sits inside the card it edits — containment does the work that a pair
   of dashed outlines was doing badly. See "Editing, without extra boxes" below. */
.edit-row { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.edit-row__label {
  font-size: var(--step--2); color: var(--text-subtle);
  text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600;
  margin-inline-start: auto;
}
.is-off { opacity: 0.45; }

.placeholder {
  display: grid; gap: 2px; justify-items: center; text-align: center;
  padding: var(--space-5); border: 2px dashed var(--accent); border-radius: var(--radius);
  color: var(--accent); text-decoration: none; background: transparent;
}
.placeholder:hover { background: var(--surface-2); }
.placeholder__plus { font-size: var(--step-2); line-height: 1; font-weight: 300; }
.placeholder__label { font-weight: 650; }
.placeholder__hint { font-size: var(--step--2); color: var(--text-subtle); }

/* The `+` sits in the tab strip as a member of it — its position is what says what
   it adds. Not a radio, so it needs the label's look without the tab's state. */
.tabs__label--add { color: var(--accent); cursor: pointer; }
.tabs__off { font-size: var(--step--2); color: var(--text-subtle); font-weight: 400; }
.offering-edit { display: grid; gap: 0; }

/* ---- Calendar operations ---------------------------------------------------
   A quiet settings stack above one dense time surface. The time ruler is the one
   distinctive element because time, not decoration, is what the owner manipulates. */
.schedule-manager { max-inline-size: 70rem; margin-inline: auto; }
/* The four calendar accordions are `.group`s that open — the same card as every
   other list on the site, with a settings-style row for a summary. They used to be
   the one surface still drawn with a 1px border, which made this screen read as a
   different product from the builder it is reached from. (2026-08-12) */
.schedule-settings {
  border: 0; border-radius: var(--radius);
  background: var(--surface); box-shadow: var(--shadow-sm); overflow: clip;
}
.schedule-settings > summary {
  display: flex; justify-content: space-between; align-items: center; gap: var(--space-3);
  padding: var(--space-4); cursor: pointer; font-weight: 650;
  min-block-size: 3rem;
}
.schedule-settings > summary:hover { background: var(--surface-2); }
.schedule-settings[open] > summary { box-shadow: inset 0 -1px 0 var(--hairline); }
/* The step badge sits inline with the accordion's title, so the two read as one
   label rather than as a number floating beside a heading. */
.schedule-settings > summary > span:first-child {
  display: flex; align-items: center; gap: var(--space-3);
}
.calendar-board__head .eyebrow { display: flex; align-items: center; gap: var(--space-3); margin: 0 0 var(--space-1); }
.schedule-settings > summary::marker { color: var(--accent); }
.schedule-settings__body { padding: 0 var(--space-4) var(--space-4); }
.schedule-settings-form { display: grid; gap: var(--space-4); }
.schedule-settings-form__actions { grid-column: 1 / -1; }
.schedule-timezone { border-block-start: 1px solid var(--border); padding-block-start: var(--space-3); }
.schedule-timezone > summary {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3);
  cursor: pointer; font-size: var(--step--1); font-weight: 650;
}
.schedule-timezone__body { padding-block-start: var(--space-3); }
.schedule-block-form .field:has(.calendar-parts),
.schedule-block-form > .cluster { grid-column: 1 / -1; }
.schedule-inline-form {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3); align-items: end;
}
.schedule-inline-form .field { margin: 0; }
.schedule-inline-form .field:has(input[name="weekdays"]) { grid-column: 1 / -1; }
.schedule-inline-form #id_weekdays {
  display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: var(--space-1);
  margin: 0; padding: 0;
}
.schedule-inline-form #id_weekdays label {
  display: grid; grid-template-columns: auto minmax(0, 1fr); place-items: center;
  min-block-size: 2.75rem; padding: var(--space-1); gap: var(--space-1);
  border: 1px solid var(--border); border-radius: var(--radius-sm); cursor: pointer;
  font-size: var(--step--1); text-align: center;
}
.schedule-inline-form #id_weekdays label:has(input:checked) {
  border-color: var(--accent); background: var(--accent-soft); color: var(--accent);
}
.schedule-inline-form #id_weekdays input { inline-size: 1rem; block-size: 1rem; accent-color: var(--accent); }
.hour-minute { display: flex; align-items: center; gap: var(--space-2); max-inline-size: 20rem; }
.hour-minute__part { display: grid; gap: var(--space-1); flex: 1; min-inline-size: 0; }
.hour-minute__part small { color: var(--text-subtle); font-size: var(--step--2); }
.hour-minute__separator { margin-block-end: var(--space-5); color: var(--text-muted); font-weight: 750; }

.calendar-board {
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--surface); padding: var(--space-4); overflow: visible;
}
.calendar-board__toolbar {
  position: sticky; z-index: 4; inset-block-start: 0;
  display: grid; gap: var(--space-3); margin: calc(-1 * var(--space-4));
  margin-block-end: 0; padding: var(--space-3) var(--space-4);
  border-block-end: 1px solid var(--border); background: var(--surface);
}
.calendar-board__head {
  display: flex; flex-wrap: wrap; align-items: end; justify-content: space-between;
  gap: var(--space-3);
}
.calendar-board__head h2 { margin: 0; }
.calendar-board__nav { display: grid; grid-template-columns: 3rem minmax(5rem, auto) 3rem; justify-content: center; gap: var(--space-2); }
.calendar-board__step { min-inline-size: 3rem; min-block-size: 3rem; padding: 0; font-size: var(--step-2); }
.calendar-detail {
  inline-size: min(34rem, calc(100% - 2 * var(--space-4))); max-block-size: calc(100dvh - 2 * var(--space-4));
  padding: 0; border: 0; border-radius: var(--radius-lg); background: transparent;
  box-shadow: var(--shadow-lg); overflow: auto;
}
.calendar-detail::backdrop { background: var(--overlay); }
.calendar-inspector {
  padding: var(--space-5); border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--surface);
}
.calendar-inspector__head { display: flex; justify-content: space-between; gap: var(--space-3); }
.calendar-inspector h3 { margin: 0; }
.calendar-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); margin: 0; }
.calendar-facts div { padding: var(--space-2); border-radius: var(--radius-sm); background: var(--surface); }
.calendar-facts dt { color: var(--text-muted); font-size: var(--step--1); }
.calendar-facts dd { margin: 0; font-weight: 650; }
.segmented {
  display: inline-grid; grid-auto-flow: column; padding: var(--space-1);
  border-radius: var(--radius-pill); background: var(--surface-2);
}
.segmented a {
  padding: var(--space-2) var(--space-3); border-radius: var(--radius-pill);
  color: var(--text-muted); font-size: var(--step--1); font-weight: 650;
}
.segmented a[aria-current="page"] { background: var(--surface); color: var(--accent); box-shadow: var(--shadow-sm); }

.time-board { display: grid; gap: var(--space-2); overflow-x: auto; padding-block-end: var(--space-2); }
.time-board--day { grid-template-columns: minmax(18rem, 1fr); }
.time-board--week { grid-template-columns: repeat(7, minmax(9rem, 1fr)); }
.time-day { min-inline-size: 0; }
.time-day > h3 {
  display: flex; justify-content: space-between; gap: var(--space-2);
  padding: var(--space-2); font-size: var(--step--1); color: var(--text-muted);
}
.time-day__track {
  position: relative; block-size: 36rem; overflow: hidden;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-2);
}
.time-hour {
  display: block; block-size: 1.5rem; padding-inline: var(--space-1);
  border-block-start: 1px solid var(--border); color: var(--text-subtle);
  font-size: var(--step--1); font-variant-numeric: tabular-nums;
}
.calendar-slot, .calendar-event {
  position: absolute; z-index: 1; inset-inline: 2.5rem var(--space-1);
  inset-block-start: var(--calendar-start); block-size: max(var(--calendar-span), 1.6rem);
  padding: var(--space-1) var(--space-2); overflow: hidden;
  border: 1px solid var(--accent); border-radius: var(--radius-sm);
  font: inherit; font-size: var(--step--2); text-align: start;
  text-decoration: none;
}
.calendar-slot {
  background: var(--accent-soft); color: var(--accent); cursor: pointer;
}
.calendar-slot:hover { background: var(--accent); color: var(--on-accent); }
.calendar-slot small, .calendar-event span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.calendar-event { z-index: 2; background: var(--surface); color: var(--text); border-color: var(--border-strong); }
.calendar-event--booking { background: var(--success-soft); border-color: var(--success); }
.calendar-event--maintenance { background: var(--danger-soft); border-color: var(--danger); }
.calendar-event--default-unavailable,
.calendar-event--default-busy,
.calendar-event--default-maintenance { z-index: 0; border-style: dashed; color: var(--text-muted); }
.calendar-event--default-maintenance { background: var(--danger-soft); border-color: var(--danger); }

.month-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 1px; background: var(--border); }
.month-weekday {
  padding-block: var(--space-2); background: var(--surface-2); color: var(--text-muted);
  font-size: var(--step--1); font-weight: 700; text-align: center;
}
.month-weekday__short { display: none; }
.month-day {
  min-block-size: 5rem; padding: var(--space-2); background: var(--surface);
  color: var(--text); display: flex; justify-content: space-between; align-items: start;
}
.month-day:hover { background: var(--accent-soft); }
.month-day.is-outside { color: var(--text-subtle); background: var(--surface-2); }
.month-day__count {
  display: grid; place-items: center; min-inline-size: 1.5rem; block-size: 1.5rem;
  padding-inline: var(--space-1); border-radius: var(--radius-pill);
  background: var(--accent); color: var(--on-accent); font-size: var(--step--2);
}

/* The public picker is a readable set of native disclosures without JavaScript.
   Enhancement turns the same radios into a compact date rail and time-chip grid. */
.appointment-picker { display: grid; gap: var(--space-4); min-inline-size: 0; margin: 0; padding: 0; border: 0; }
.appointment-dates { display: none; gap: var(--space-2); overflow-x: auto; padding-block-end: var(--space-1); scrollbar-width: thin; }
.appointment-picker.is-enhanced .appointment-dates { display: flex; }
.appointment-date {
  flex: 0 0 auto; display: grid; gap: var(--space-1); min-inline-size: 5.5rem; min-block-size: 3.5rem;
  place-content: center; padding: var(--space-2) var(--space-3); border: 1px solid var(--border);
  border-radius: var(--radius); background: var(--surface); color: var(--text); cursor: pointer;
}
.appointment-date span { color: var(--text-muted); font-size: var(--step--2); }
.appointment-date.is-active { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
.appointment-day { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); }
.appointment-day > summary { padding: var(--space-3); cursor: pointer; font-weight: 650; }
.appointment-picker.is-enhanced .appointment-day { border: 0; }
.appointment-picker.is-enhanced .appointment-day > summary { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.appointment-times { display: grid; grid-template-columns: repeat(auto-fill, minmax(5.25rem, 1fr)); gap: var(--space-2); padding: var(--space-3); }
.appointment-time { position: relative; cursor: pointer; }
.appointment-time input { position: absolute; opacity: 0; pointer-events: none; }
.appointment-time span {
  display: grid; place-items: center; min-block-size: 2.75rem; padding: var(--space-2);
  border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text);
  font-variant-numeric: tabular-nums;
}
.appointment-time input:checked + span { border-color: var(--accent); background: var(--accent); color: var(--on-accent); }
.appointment-time input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 2px; }
.appointment-time:hover span { border-color: var(--accent); }
/* Remaining places under a shared time slot. Only rendered when the service takes
   more than one booking at a time and the owner chose to show counts. */
.appointment-time__left {
  display: block; text-align: center;
  margin-block-start: 2px;
  font-size: var(--step--2); color: var(--text-subtle);
}
.appointment-time__left--last { color: var(--danger); font-weight: 600; }
.appointment-time input:checked ~ .appointment-time__left { color: var(--on-accent); }
.appointment-length { max-inline-size: 24rem; }
.appointment-summary { margin: 0; padding: var(--space-3); border-radius: var(--radius-sm); background: var(--surface-2); white-space: pre-line; }

@media (max-width: 40rem) {
  .panel--calendar { max-inline-size: 100%; margin-inline: 0; border-radius: var(--radius-lg) var(--radius-lg) 0 0; }
  .schedule-inline-form { grid-template-columns: 1fr; }
  .schedule-settings-form__fields { grid-template-columns: 1fr; }
  .schedule-inline-form #id_weekdays { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .calendar-facts { grid-template-columns: 1fr; }
  .calendar-board { margin-inline: calc(-1 * var(--space-4)); border-inline: 0; border-radius: 0; }
  .calendar-board__toolbar { inset-block-start: 0; }
  .calendar-board__head { align-items: stretch; }
  .calendar-board__head .segmented { inline-size: 100%; }
  .calendar-board__head .segmented a { text-align: center; }
  .calendar-detail { inline-size: calc(100% - 2 * var(--space-3)); }
  .time-board--week { grid-template-columns: repeat(7, minmax(12rem, 1fr)); }
  .month-day { min-block-size: 3.5rem; padding: var(--space-1); }
  .month-weekday { padding-inline: 0; }
  .month-weekday__long { display: none; }
  .month-weekday__short { display: inline; }
}
/* Calendar date parts remain explicit because native date inputs submit Gregorian
   values even when their browser chrome appears localized. */
.calendar-parts {
  display: grid;
  grid-template-columns: minmax(6rem, 1.15fr) minmax(8rem, 1.5fr) minmax(5rem, .8fr);
  gap: var(--space-2);
}
.calendar-parts:has(> :nth-child(5)) {
  grid-template-columns: minmax(6rem, 1.15fr) minmax(8rem, 1.5fr) repeat(3, minmax(4.5rem, .75fr));
}
@media (max-width: 40rem) {
  .calendar-parts,
  .calendar-parts:has(> :nth-child(5)) { grid-template-columns: 1fr 1fr; }
  .calendar-parts > :first-child { grid-column: 1 / -1; }
}

/* Pagination: centred, roomy enough to tap, and logical-property safe for RTL. */
.pagination {
  justify-content: center;
  align-items: center;
  gap: var(--space-3);
  margin-block-start: var(--space-6);
  flex-wrap: wrap;
}

/* The owner's calendar shortcut, sitting before a service's name on its accordion.
   Frequent enough that hunting for it in a handle strip was the wrong trade. */
.offering__calendar {
  display: inline-grid; place-items: center;
  inline-size: 2.25rem; block-size: 2.25rem;
  margin-inline-end: var(--space-2);
  border-radius: 50%; border: 1px solid var(--border);
  background: var(--surface); color: var(--accent); text-decoration: none;
  flex: none;
}
.offering__calendar:hover { background: var(--surface-2); border-color: var(--accent); }

/* Per-domain footer: what this site is, then the policies. Never the menu again. */
.footer-about { max-inline-size: 42ch; }
.footer-name { font-weight: 600; color: var(--text); }
.footer-text {
  margin-block-start: var(--space-2); margin-block-end: 0;
  font-size: var(--step--1); color: var(--text-muted); line-height: 1.6;
}
.footer-links { gap: var(--space-4); flex-wrap: wrap; }

/* The notification bell. A count, not a dot: "you have three things" is actionable,
   "you have something" only prompts a click to find out. */
.nav-bell { position: relative; display: inline-grid; place-items: center; }
.nav-bell__dot {
  position: absolute; inset-block-start: -0.15rem; inset-inline-end: -0.35rem;
  min-inline-size: 1.15rem; padding: 0 0.25rem;
  border-radius: var(--radius-pill);
  background: var(--accent); color: var(--on-accent);
  font-size: var(--step--2); font-weight: 700; line-height: 1.15rem; text-align: center;
}

/* =============================================================================
   App layer — redesign 2026-08-04
   -----------------------------------------------------------------------------
   The direction is the owner's: Apple/iOS and recent Telegram. That is not a
   palette, it is a set of *idioms*, and the page was missing all of them:

   - **Grouped inset lists.** iOS Settings is the reference — content lives in
     rounded cards floating on a tinted page, with hairline separators inset from
     the leading edge. It reads as "sections of one thing" rather than a stack of
     unrelated boxes.
   - **A bottom tab bar on mobile.** Most visitors are on a phone; a hamburger at
     the top of a long page puts navigation where the thumb is not.
   - **Folder-style section tabs.** The signature. Telegram's chat folders are
     exactly the metaphor this product needs: one profile that is several
     businesses, switched between without leaving.

   Tokens are unchanged on purpose. Repainting the palette would touch forty
   templates to solve a problem that is compositional, not chromatic.
   ========================================================================== */

:root {
  /* A tinted page so white cards read as raised. The single most characteristic
     thing about the iOS grouped look, and it costs one value. */
  --page-tint: #f2f2f7;
  --hairline: rgba(60, 60, 67, 0.16);
  --tabbar-height: 3.75rem;
  --safe-bottom: env(safe-area-inset-bottom, 0px);
}

@media (prefers-color-scheme: dark) {
  :root { --page-tint: #000000; --hairline: rgba(84, 84, 88, 0.5); }
}
:root[data-theme="light"] { --page-tint: #f2f2f7; --hairline: rgba(60, 60, 67, 0.16); }
:root[data-theme="dark"] { --page-tint: #000000; --hairline: rgba(84, 84, 88, 0.5); }

/* ---- Grouped inset list --------------------------------------------------- */
.group {
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}
/* Corners rounded by the first and last row, **not** by `overflow: hidden` on the
   container.
   -----------------------------------------------------------------------------
   `overflow: hidden` was here to clip the rows to the card's radius, and the owner
   found it clipping rows away entirely: the Balance and Notifications rows were in
   the HTML and not on the screen. Clipping is the wrong tool for this job anyway —
   it also cuts focus rings on the rows inside, which quietly breaks keyboard use
   on the one component the whole settings interface is built from.

   Rounding the ends achieves the same look with nothing hidden, and cannot swallow
   content whatever the container ends up being sized to. */
.group > *:first-child {
  border-start-start-radius: var(--radius);
  border-start-end-radius: var(--radius);
}
.group > *:last-child {
  border-end-start-radius: var(--radius);
  border-end-end-radius: var(--radius);
}
.group + .group { margin-block-start: var(--space-5); }
/* The heading sits *outside* the card, quiet and uppercase — iOS puts section
   titles above the group, not inside it, which is what makes the group read as a
   single object rather than a titled box. */
.group-title {
  display: block;
  font-size: var(--step--1); font-weight: 600;
  color: var(--text-subtle);
  text-transform: uppercase; letter-spacing: 0.06em;
  padding-inline: var(--space-4);
  margin-block: var(--space-6) var(--space-2);
}
.group__item {
  display: flex; align-items: center; gap: var(--space-3);
  min-block-size: 3rem; padding: var(--space-3) var(--space-4);
}
/* Separators inset from the leading edge only — the detail that makes a list feel
   native rather than like a table. */
/* Any row after any other row, not only a matching pair. A group holding both
   `.list-row` and `.settings-row` — which the settings sheet's "Recently" group
   does — was losing the separator at the seam between the two kinds. */
.group > * + * { box-shadow: inset 0 1px 0 var(--hairline); }

/* ---- Profile identity -----------------------------------------------------
   Avatar beside the name rather than above it, and not centred. Centring cost a
   whole phone screen of height before anything the visitor came for, and it made
   the profile look like a person's social page rather than a business's front
   door. Side by side is what Instagram and LinkedIn both settled on, for the same
   reason: the identity is a header, not a hero image. */
.identity {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: "avatar who" "meta meta" "metrics metrics" "actions actions";
  align-items: center;
  gap: var(--space-3) var(--space-4);
  padding-block: var(--space-5) var(--space-4);
}
.identity > .avatar { grid-area: avatar; }
.identity__who { grid-area: who; min-inline-size: 0; }
.identity__name {
  font-size: var(--step-2); font-weight: 700; letter-spacing: -0.02em;
  line-height: 1.15; margin: 0;
  overflow-wrap: anywhere;
}
/* `@slug`, correct in both directions.
   -----------------------------------------------------------------------------
   This was `direction: ltr` on a block element, which does not do what it looks
   like it does. Direction sets the *inline axis of the box*, so on the Persian
   site the block's start edge became the left one and the handle jumped to the
   far side of the page, away from the name it belongs to. The owner reported
   exactly that.

   `unicode-bidi: isolate` alone was not enough, and the second report said why:
   the handle rendered as `username@`. Isolation stops the run from mixing with the
   Persian around it, but the `@` is a neutral character and takes its direction
   from the *paragraph*, which is RTL — so it swapped to the trailing side.

   The pair that is actually correct: `direction: ltr` to order the run, plus
   `inline-size: fit-content` so the box hugs the text instead of stretching to
   the other margin. `direction` on a **fit-content** box sets the text's axis
   without moving the box, which is what the earlier fix was reaching for.

   The templates also carry `dir="ltr"`, so the run is right even before CSS
   loads and in any context that copies the markup out. */
.identity__handle {
  display: block;
  inline-size: fit-content;
  font-size: var(--step-0); color: var(--text-subtle);
  direction: ltr;
  unicode-bidi: isolate;
}
.identity__meta {
  grid-area: meta;
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-1) var(--space-3);
  font-size: var(--step--1); color: var(--text-muted);
  margin: 0;
}
.identity__meta > * { display: inline-flex; align-items: center; gap: 0.35em; }
.identity__meta .icon { inline-size: 1em; block-size: 1em; flex: none; }
.identity__meta a { color: inherit; text-decoration: none; }
.identity__meta a:hover { color: var(--accent); text-decoration: underline; }
/* Dot separators drawn by CSS rather than typed into the template, so a hidden
   rating or a missing location never leaves a dangling bullet. */
.identity__meta > * + *::before {
  content: "·"; color: var(--text-subtle);
  margin-inline-end: 0.35em;
}
.identity__actions {
  grid-area: actions;
  display: flex; gap: var(--space-2); flex-wrap: wrap;
}
.identity__actions .btn { flex: 1 1 9rem; }

/* The owner's two controls, pinned to the top corner of the header where iOS puts
   navigation-bar buttons. They are chrome for this page, not content on it, so
   they must not push the name down or sit in the visitor's reading order. */
.identity__tools {
  grid-area: who; justify-self: end; align-self: start;
  display: flex; gap: var(--space-1);
}
.icon-btn {
  display: inline-grid; place-items: center; position: relative;
  inline-size: 2.25rem; block-size: 2.25rem;
  border-radius: 50%; border: 0;
  background: var(--surface-2); color: var(--text-muted);
  text-decoration: none; cursor: pointer;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.icon-btn:hover { background: var(--accent-soft); color: var(--accent); }
.icon-btn:focus-visible { outline: none; box-shadow: var(--ring); }
.icon-btn__badge {
  position: absolute; inset-block-start: -0.15rem; inset-inline-end: -0.15rem;
  min-inline-size: 1.1rem; padding-inline: 0.2rem;
  border-radius: var(--radius-pill);
  background: var(--danger); color: #fff;
  font-size: 0.6rem; font-weight: 700; line-height: 1.1rem; text-align: center;
}

@media (min-width: 40rem) {
  .identity {
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas: "avatar who tools" "avatar meta tools" "metrics metrics metrics" "actions actions actions";
    align-items: start;
  }
  .identity > .avatar { grid-row: span 2; align-self: center; }
  .identity__tools { grid-area: tools; justify-self: end; }
  .identity__actions { justify-content: flex-start; }
  .identity__actions .btn { flex: 0 0 auto; }
}

/* ---- The insight summary --------------------------------------------------
   Three numbers under the name, tappable as one control that opens the full
   analytics. The owner asked for a summary "near user's name and profile photo"
   that opens a modal — so this is deliberately a *button*, not three links: the
   summary's job is to be read at a glance and to be one target for the thumb. */
.metrics {
  grid-area: metrics;
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  inline-size: 100%; gap: var(--space-1);
  padding: var(--space-1);
  border: 0; border-radius: var(--radius);
  background: var(--surface-2); color: inherit;
}
/* Each cell is its own target now, so the hover and focus states moved from the
   strip to the cells. A number that reports something should go to it. */
.metric {
  display: grid; gap: 1px; justify-items: center; align-content: center;
  padding-block: var(--space-2);
  border-radius: calc(var(--radius) - var(--space-1));
  color: inherit; text-decoration: none;
  transition: background var(--dur) var(--ease);
}
a.metric:hover { background: var(--accent-soft); color: var(--accent); }
a.metric:focus-visible { outline: none; box-shadow: var(--ring); }
.metric--more .metric__value { color: var(--accent); }
.metric--more .icon { inline-size: 1.25rem; block-size: 1.25rem; }
.insight-strip a.metric:hover { background: var(--surface-2); }
.metric__value {
  font-size: var(--step-1); font-weight: 700; letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums; line-height: 1.1;
}
.metric__label {
  font-size: var(--step--2); color: var(--text-muted);
  text-transform: uppercase; letter-spacing: 0.05em;
}
/* No separator between cells any more. Each one is a rounded, individually
   hoverable target, and a hairline through the middle of a hover fill reads as a
   rendering fault rather than as structure. */

/* ---- Folder tabs: the signature ------------------------------------------
   One profile is several businesses. These are not filters and they are not
   page navigation — switching one changes everything below it, which is why
   they are attached to the identity above rather than floating over content.

   The radio/label/panel adjacency is load-bearing: `input:checked + label +
   panel` does the work with no script and no per-profile ids, so no wrapper may
   be introduced around the labels. */
.tabs {
  display: flex; flex-wrap: wrap; gap: var(--space-2);
  align-items: flex-start;
}
.tabs__radio {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}
.tabs__label {
  order: 1; cursor: pointer; white-space: nowrap;
  display: inline-flex; align-items: center; gap: var(--space-2);
  min-block-size: 2.5rem; padding-inline: var(--space-4);
  border-radius: var(--radius-pill);
  background: var(--surface-2); border: 1px solid transparent;
  font-size: var(--step-0); font-weight: 600; color: var(--text-muted);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              transform var(--dur) var(--ease);
}
.tabs__label:hover { color: var(--text); background: var(--accent-soft); }
.tabs__label:active { transform: scale(0.97); }
.tabs__radio:checked + .tabs__heading .tabs__label {
  background: var(--accent); color: var(--on-accent);
  box-shadow: 0 4px 12px var(--accent-soft);
}
.tabs__radio:focus-visible + .tabs__heading .tabs__label { outline: none; box-shadow: var(--ring); }

/* The division the owner asked for.
   -----------------------------------------------------------------------------
   Highlighting the selected pill was not enough: *"it doesn't divide the page in
   my mental model"*. A tab that only recolours a word leaves the page below it
   looking like the same continuous page it was before, so nothing tells you that
   what you are reading belongs to the niche you picked.

   So the panel is now a physical container — it bleeds to the edge of the layout,
   sits on the tinted page colour with a hairline above it, and carries the
   section's own name as a caption. Everything inside it visibly *is* that
   section. The tint stops at the container's edge rather than the viewport's,
   which reads as a large card on a desktop and as a full-width sheet on a phone,
   and avoids the 100vw-versus-scrollbar overflow bug entirely.

   `input:checked + label + panel` still does the showing, so no wrapper may come
   between these three elements. */
.tabs__panel {
  order: 2; flex: 1 1 100%; display: none;
  margin-inline: calc(-1 * var(--gutter));
  margin-block-start: var(--space-4);
  padding: var(--space-5) var(--gutter) var(--space-6);
  background: var(--page-tint);
  box-shadow: inset 0 1px 0 var(--hairline);
}
.tabs__radio:checked + .tabs__heading + .tabs__panel { display: block; }
@media (min-width: 48rem) {
  .tabs__panel {
    margin-inline: 0; padding-inline: var(--space-5);
    border-radius: var(--radius-lg);
    box-shadow: none;
  }
}
/* The tab, as a heading.
   -----------------------------------------------------------------------------
   `<h2>` around the label, carrying the pill's layout so nothing about the rail
   changes: it is a heading in the document outline and a control on the screen.
   The `order`, and every `+` in the rules above, moved onto it — it is the flex
   item now, and it is the element sitting between the radio and the panel.

   Resetting `font` is not optional. Left at the `h2` scale these pills would be
   set at `--step-2`, which is the size of the *page's* section titles, on a row of
   five of them. */
.tabs__heading {
  order: 1; margin: 0; display: inline-flex;
  font-size: inherit; font-weight: inherit; letter-spacing: normal;
}

/* Sticky, because the caption that used to name the panel is gone.
   -----------------------------------------------------------------------------
   The caption printed the tab's own name again, with a dot, at the top of its
   panel — the owner asked what it was for, and on a wide screen the honest answer
   is nothing. Its one real job was the phone, where the rail scrolls away and
   leaves no sign of which niche you are reading.

   So the rail keeps itself on screen instead of repeating itself into the panel.

   **Why the labels stick, and not a wrapper around them.** `input:checked +
   heading + panel` is what shows a panel without JavaScript, so no element may sit
   around the rail — that constraint is the reason this was left undone twice. Each
   heading sticks on its own at the same offset, which works while they are on one
   line and slides line two over line one the moment they wrap. Hence the line
   below: they shrink and truncate rather than wrap, so there is only ever one
   line to stick. A profile carries a handful of niches, and a truncated pill next
   to a legible selected one is the same trade a segmented control makes. */
.tabs__heading {
  position: sticky; inset-block-start: 0; z-index: 5;
  min-inline-size: 0; flex: 0 1 auto;
}
.tabs__heading .tabs__label { min-inline-size: 0; overflow: hidden; }
.tabs__heading .tabs__label { text-overflow: ellipsis; display: flex; }
/* The rail needs its own ground while it floats over the panel, or the panel's
   content reads straight through it. */
.tabs { --rail-bg: var(--bg); }
.tabs__heading::before {
  content: ""; position: absolute; inset: calc(-1 * var(--space-2)) 0;
  background: var(--rail-bg); z-index: -1;
}
.tabs__label--add {
  color: var(--accent); background: transparent;
  border-style: dashed; border-color: var(--border-strong);
}
.tabs__off {
  font-size: var(--step--2); font-weight: 500; opacity: 0.7;
}

/* ---- Pane heads -----------------------------------------------------------
   A section panel holds four different kinds of thing — services, content,
   pictures, comments — and they used to run together into one column with no
   seam. This is the iOS grouped-list header: a quiet uppercase title, a count,
   and the one action that belongs to that group, aligned to the trailing edge.

   The count is part of the title because "Services 3" answers a question the
   owner has before they have finished reading the word. */
.pane-head {
  display: flex; align-items: center; gap: var(--space-2);
  margin-block: var(--space-6) var(--space-3);
}
.pane-head:first-child { margin-block-start: 0; }
.pane-head__title {
  margin: 0; font-size: var(--step--1); font-weight: 600;
  color: var(--text-subtle);
  text-transform: uppercase; letter-spacing: 0.06em;
}
/* Inside a card the head is the card's own title, not a label above a group, so it
   takes the weight a heading should. The locations card picked up the small
   uppercase treatment when it was converted to a pane head, and read as a caption
   for something above it rather than as the title of what it sits on. */
.card > .pane-head .pane-head__title {
  font-size: var(--step-2);
  color: var(--text);
  text-transform: none;
  letter-spacing: -0.01em;
}
.pane-head__count {
  font-variant-numeric: tabular-nums;
  color: var(--text-subtle); font-weight: 500;
}
.pane-head__action { margin-inline-start: auto; }

/* ---- The per-section insight strip ----------------------------------------
   Owner-only, at the top of the section it describes. Placed above the bio
   rather than below it on purpose: an owner opening their own profile is
   checking how it is doing, and a visitor never sees this at all, so nothing is
   pushed down for the person who came to read.

   Content blocks get no equivalent — the owner's own call, and the right one:
   a view count on a paragraph is a number nobody can act on. */
.insight-strip {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  gap: var(--space-1);
  padding: var(--space-3) var(--space-2);
  border-radius: var(--radius);
  background: var(--surface);
  margin-block-end: var(--space-4);
}
.insight-strip .metric__value { font-size: var(--step-0); }

/* ---- Editing, without extra boxes -----------------------------------------
   The first attempt wrapped every editable thing in a dashed strip plus a dashed
   subject. It made the page busier than the thing it was meant to clarify.

   Containment does the same job for free: the controls sit *inside* the card
   they act on, in its own header row. Nothing new is drawn, and there is no
   question which section a Remove belongs to because it is inside that section. */
.editable { position: relative; }
.editable__bar {
  display: flex; align-items: center; gap: var(--space-1);
  padding-block-end: var(--space-2);
  margin-block-end: var(--space-2);
  box-shadow: inset 0 -1px 0 var(--hairline);
}
/* `margin-inline-end: auto` on the *label* — so the label takes the slack and the
   tools stay at the leading edge, beside the gear, where every other strip has them.
   It used to be the other way round, which pushed both of these strips to the far
   left of a right-to-left page. */
.editable__what {
  font-size: var(--step--2); font-weight: 600; color: var(--text-subtle);
  text-transform: uppercase; letter-spacing: 0.05em;
  margin-inline-end: auto;
}
/* A strip with nothing to caption: no rule, no spacing of its own. Three of these
   were spelled as an inline `style=` repeating the same four declarations. */
.editable__bar--bare {
  margin-block: var(--space-2) 0; padding: 0; box-shadow: none;
}
/* On a pointer device the controls stay quiet until wanted; on touch there is no
   hover, so they are always legible. */
@media (hover: hover) {
  .editable__bar .edit-tools { opacity: 0.55; transition: opacity var(--dur) var(--ease); }
  .editable:hover .editable__bar .edit-tools,
  .editable:focus-within .editable__bar .edit-tools { opacity: 1; }
}

/* ---- Mobile tab bar -------------------------------------------------------
   Fixed to the bottom, where a thumb is. Hidden on wide screens, where the
   header menu is already reachable. */
.tabbar {
  position: fixed; inset-inline: 0; inset-block-end: 0; z-index: 50;
  display: flex; align-items: stretch;
  block-size: calc(var(--tabbar-height) + var(--safe-bottom));
  padding-block-end: var(--safe-bottom);
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  backdrop-filter: saturate(180%) blur(20px);
  box-shadow: inset 0 1px 0 var(--hairline);
}
.tabbar__item {
  flex: 1; display: grid; place-items: center; gap: 2px;
  padding-block: var(--space-2);
  color: var(--text-subtle); text-decoration: none;
  font-size: 0.65rem; font-weight: 500;
}
.tabbar__item[aria-current="page"] { color: var(--accent); }
.tabbar__item .icon { inline-size: 1.5rem; block-size: 1.5rem; }
.tabbar__badge {
  position: absolute; margin-inline-start: 1.1rem; margin-block-start: -0.5rem;
  min-inline-size: 1rem; padding-inline: 0.2rem;
  border-radius: var(--radius-pill);
  background: var(--danger); color: #fff;
  font-size: 0.6rem; font-weight: 700; line-height: 1rem; text-align: center;
}
@media (min-width: 48rem) { .tabbar { display: none; } }
@media (max-width: 47.99rem) {
  body { padding-block-end: calc(var(--tabbar-height) + var(--safe-bottom)); }
}

@media (prefers-reduced-motion: reduce) {
  .tabs__label, .editable__bar .edit-tools { transition: none; }
  .tabs__label:active { transform: none; }
}

/* Search: one field, one button, results grouped by what they are. */
.search-form { display: flex; gap: var(--space-2); }
.search-form .input { flex: 1; }
.group__item[href] { color: inherit; text-decoration: none; }
.group__item[href]:hover { background: var(--surface-2); }

/* =============================================================================
   App layer II — navigation, the account sheet, search  (2026-08-04)
   -----------------------------------------------------------------------------
   The brief: *"treating profile as one place to do everything that user need to
   do with it's account and profile, like what people can do in instagram or
   linkedin"*, with settings that feel like iOS Settings, and a menu whose
   rightmost item is the person's own face.

   Two ideas do most of the work here.

   **Account things and category things are different things.** The old header
   mixed a market's categories with Dashboard, Requests, Balance and Sign out in
   one flat list, so the menu grew every time the product did. Now the start of
   the bar is navigation — where you can go on this site — and the end of it is
   you: search, notifications, and your avatar. Nothing else is ever added to the
   end cluster; new account destinations go inside the sheet.

   **The sheet is the settings app.** One `<dialog>` of grouped inset lists,
   opened by the gear, reachable from the profile and from the header. It is the
   only place Sign out appears, at the bottom, in red, exactly where iOS puts a
   destructive row — which is also how it stopped being a first click.
   ========================================================================== */

/* ---- Header end cluster --------------------------------------------------- */
.nav-you {
  display: flex; align-items: center; gap: var(--space-1);
  margin-inline-start: auto;
}
/* The burger keeps the categories; the account cluster is never inside it, so a
   phone still reaches notifications and the profile in one tap. */
.nav-you .icon-btn { background: transparent; }
.nav-you .icon-btn:hover { background: var(--accent-soft); }

/* The avatar is the button. A person recognises their own picture faster than
   they read the word "Account", which is the entire reason every social product
   converged on this. */
.usermenu { position: relative; }
.usermenu > summary {
  list-style: none; cursor: pointer;
  display: inline-grid; place-items: center;
  border-radius: 50%;
}
.usermenu > summary::-webkit-details-marker { display: none; }
.usermenu > summary:focus-visible { outline: none; box-shadow: var(--ring); }
.usermenu[open] > summary .avatar { box-shadow: 0 0 0 2px var(--accent); }
.usermenu__panel {
  position: absolute; inset-block-start: calc(100% + var(--space-2));
  inset-inline-end: 0; z-index: 60;
  inline-size: max-content; min-inline-size: 14rem; max-inline-size: 18rem;
  padding: var(--space-2);
  border-radius: var(--radius);
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-lg);
}
.usermenu__panel a,
.usermenu__panel button {
  display: flex; align-items: center; gap: var(--space-3);
  inline-size: 100%; min-block-size: 2.5rem;
  padding: var(--space-2) var(--space-3);
  border: 0; border-radius: var(--radius-sm);
  background: transparent; color: var(--text);
  font: inherit; text-align: start; text-decoration: none; cursor: pointer;
}
.usermenu__panel a:hover,
.usermenu__panel button:hover { background: var(--surface-2); }
.usermenu__panel .icon { color: var(--text-subtle); flex: none; }
.usermenu__head {
  padding: var(--space-2) var(--space-3) var(--space-3);
  border-block-end: 1px solid var(--hairline);
  margin-block-end: var(--space-2);
}
.usermenu__name { display: block; font-weight: 600; }
.usermenu__handle {
  display: block; inline-size: fit-content;
  font-size: var(--step--1); color: var(--text-subtle);
  direction: ltr; unicode-bidi: isolate;
}
@media (max-width: 47.99rem) {
  /* On a phone the tab bar already carries all of this, and a second copy in the
     header is one more thing between the visitor and the page. */
  .nav-you { display: none; }
}

/* ---- The account sheet: iOS Settings --------------------------------------
   Grouped inset lists on a tinted sheet, each row a label, an optional trailing
   value, and a chevron. The value on the right is what makes a settings list
   readable without opening anything — you learn your balance and how many
   requests are waiting from the list itself. */
.sheet--settings::backdrop { background: var(--overlay); }
/* Only the colour differs. The padding must stay identical to every other sheet,
   because the sticky title cancels it with a negative margin and a variant that
   changes it leaves the title inset by the difference. */
.sheet--settings .sheet__body { background: var(--page-tint); }
.sheet__title {
  display: flex; align-items: center; gap: var(--space-3);
  margin-block-end: var(--space-4);
}
.sheet__title h2 { margin: 0; font-size: var(--step-1); }
.sheet__title .icon-btn { margin-inline-start: auto; }

.settings-row {
  display: flex; align-items: center; gap: var(--space-3);
  min-block-size: 2.9rem; padding: var(--space-2) var(--space-4);
  color: var(--text); text-decoration: none;
  background: none; border: 0; inline-size: 100%; font: inherit;
  text-align: start; cursor: pointer;
}
.settings-row + .settings-row { box-shadow: inset 0 1px 0 var(--hairline); }
.settings-row:hover { background: var(--surface-2); }
.settings-row__icon {
  display: grid; place-items: center; flex: none;
  inline-size: 1.75rem; block-size: 1.75rem;
  border-radius: 7px;
  background: var(--accent); color: var(--on-accent);
}
.settings-row__icon .icon { inline-size: 1rem; block-size: 1rem; }
.settings-row__label { flex: 1; min-inline-size: 0; }
.settings-row__value {
  color: var(--text-subtle); font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* A chevron that points the way the language runs. `scale(-1,1)` rather than a
   second glyph, so one icon serves both directions. */
.settings-row__go {
  flex: none; color: var(--text-subtle); opacity: 0.6;
  inline-size: 1rem; block-size: 1rem;
}
[dir="rtl"] .settings-row__go { transform: scale(-1, 1); }
.settings-row--danger { color: var(--danger); }
.settings-row--danger .settings-row__icon { background: var(--danger); color: #fff; }
/* Tinted per group, so the list scans by colour before it is read — the one
   thing iOS Settings does that makes a long list navigable at a glance. */
.settings-row--money .settings-row__icon { background: var(--gold); color: #fff; }
.settings-row--alert .settings-row__icon { background: #cf6b1e; color: #fff; }

/* ---- Mobile tab bar, revised ----------------------------------------------
   Four destinations, the last one being the person. Home · Search · Alerts ·
   Profile, reading from the leading edge, so in an RTL market the profile lands
   under the right thumb exactly as the owner asked — logical properties give
   that for free and it flips correctly if a market ever runs LTR.

   Dashboard has left the bar. It was occupying a permanent slot to reach a page
   that is now a sheet inside the profile, which is where the owner wanted it. */
/* The tab bar is the one place in this stylesheet that is deliberately *physical*
   rather than logical.
   -----------------------------------------------------------------------------
   The owner's correction, 2026-08-04: *"I want the profile on the right for both
   rtl and ltr versions and then the current order which is profile, notifications,
   search, home, starting from right to left, both on rtl and ltr these can stay
   the same."*

   Logical properties gave the opposite — in RTL the leading edge is the right, so a
   DOM order of Home · Search · Alerts · Profile put the profile on the *left*.
   Forcing the bar to `direction: ltr` pins that DOM order to left-to-right
   regardless of the page, so Profile is the rightmost item in every language.

   This is right and it is not an inconsistency. Everywhere else the interface
   mirrors because it is *reading* — text, forms, lists all follow the language. A
   thumb does not. The bar is a physical control at the bottom of a physical device,
   and the person holding it has the same hand in Tehran and in Zurich.

   Only the bar's own axis is forced; each item's contents stay logical, so an icon
   and its label still order correctly for the language inside their cell. */
.tabbar { direction: ltr; }
.tabbar__item { position: relative; text-align: center; direction: inherit; }
[dir="rtl"] .tabbar__item { direction: rtl; }
.tabbar__item[aria-current="page"] { color: var(--accent); font-weight: 600; }
/* Apple indicates the active tab by colour and weight, not by an underline or a
   pill. The one addition is a small dot for the profile tab, because an avatar
   photo cannot be tinted the way an icon can. */
.tabbar__item--you[aria-current="page"]::after {
  content: ""; position: absolute; inset-block-end: 0.35rem;
  inline-size: 4px; block-size: 4px; border-radius: 50%;
  background: var(--accent);
}
.tabbar__item--you[aria-current="page"] .avatar { box-shadow: 0 0 0 2px var(--accent); }
.tabbar__item .avatar { inline-size: 1.6rem; block-size: 1.6rem; font-size: 0.7rem; }

/* ---- Search ---------------------------------------------------------------
   One field, mixed results, each row saying what kind of thing it is by its
   leading glyph: a pin is a place, a tag is a niche, a picture is a business.
   Instagram's search does exactly this and nobody needs it explained. */
.searchbar {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--surface-2);
}
.searchbar .icon { color: var(--text-subtle); flex: none; }
.searchbar input {
  flex: 1; min-inline-size: 0;
  border: 0; background: transparent; color: var(--text);
  font: inherit; font-size: var(--step-0);
}
.searchbar input:focus { outline: none; }
.searchbar:focus-within { box-shadow: var(--ring); }
/* The submit button exists for the keyboard and for no-JS, but it should not
   compete with the field on a phone. */
.searchbar button { flex: none; }

.result {
  display: flex; align-items: center; gap: var(--space-3);
  min-block-size: 3.5rem; padding: var(--space-2) var(--space-4);
  color: var(--text); text-decoration: none;
}
.result + .result { box-shadow: inset 0 1px 0 var(--hairline); }
.result:hover { background: var(--surface-2); }
.result__kind {
  display: grid; place-items: center; flex: none;
  inline-size: 2.5rem; block-size: 2.5rem;
  border-radius: 50%;
  background: var(--surface-2); color: var(--text-subtle);
}
.result__kind--place { background: var(--accent-soft); color: var(--accent); }
.result__kind--niche { background: var(--gold-soft); color: var(--gold); }
.result__body { min-inline-size: 0; flex: 1; }
.result__name {
  display: block; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The covered subcategories, exactly as the directory card head shows them —
   the same fact in the same words in both places, so a result is recognisable
   as the card it leads to. */
.result__meta {
  display: block; font-size: var(--step--1); color: var(--text-subtle);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Before anything is typed, and again when the field is cleared, the page shows
   what there is to browse rather than an empty box. An empty screen is an
   invitation to act, and a directory's answer to "what is here" is its niches. */
.catgrid {
  display: grid; gap: var(--space-3);
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
}
.cattile {
  display: grid; align-content: space-between; gap: var(--space-4);
  min-block-size: 6rem; padding: var(--space-4);
  border-radius: var(--radius);
  background: var(--surface); color: var(--text); text-decoration: none;
  box-shadow: var(--shadow-sm);
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.cattile:hover { transform: translateY(-2px); box-shadow: var(--shadow); }
.cattile .icon { color: var(--accent); }
.cattile__name { font-weight: 600; line-height: 1.25; }
.cattile__count { font-size: var(--step--1); color: var(--text-subtle); }

/* ---- Money ----------------------------------------------------------------
   A price with no unit is a number, and a number on its own is unreadable in a
   market whose prices run to seven figures. The unit is set quieter and smaller
   than the amount so the amount is still what you see first. */
.price { display: inline-flex; align-items: baseline; gap: 0.3em; }
.price__amount { font-weight: 600; font-variant-numeric: tabular-nums; }
.price__unit { font-size: 0.8em; color: var(--text-muted); font-weight: 500; }
.price__from {
  font-size: 0.8em; color: var(--text-subtle);
  margin-inline-end: 0.2em;
}

/* ---- Relative time --------------------------------------------------------
   Rendered by the server as an absolute date and rewritten in the browser to
   "3 hours ago" in the visitor's own locale and zone. Server-side relative time
   would be wrong for anyone in another zone and would poison the page cache. */
time[data-relative] { white-space: nowrap; }

@media (prefers-reduced-motion: reduce) {
  .cattile, .metrics, .icon-btn { transition: none; }
  .cattile:hover { transform: none; }
}

/* =============================================================================
   App layer III — modals, sticky tabs, live search  (2026-08-04, second pass)
   -----------------------------------------------------------------------------
   The owner on the settings sheet: *"the best ui we have now, it's so fresh and
   clean and friendly, I wish all parts of our system would be elegant and minimal
   like this."* So the sheet's treatment becomes the general one, and the rough
   edges he named on it get fixed everywhere rather than in one place.
   ========================================================================== */

/* ---- The backdrop ---------------------------------------------------------
   A blur, not just a scrim. This is what makes an iOS or macOS sheet read as
   *in front of* the page rather than drawn on top of it — the page is still
   there, still recognisable, and unmistakably not the thing you are using.

   Kept subtle: 6px is enough to defocus text without the page appearing to melt,
   and the tint underneath still carries the contrast the blur alone cannot. */
dialog::backdrop {
  background: rgba(0, 0, 0, 0.32);
  backdrop-filter: blur(6px) saturate(120%);
  -webkit-backdrop-filter: blur(6px) saturate(120%);
  animation: backdrop-in 200ms var(--ease);
}
@keyframes backdrop-in { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { dialog::backdrop { animation: none; } }
/* A phone is doing this on a compositor with a fraction of the headroom, and a
   full-screen blur behind a scrolling sheet is where it shows. Desktop is where
   the owner asked for it — *"especially on desktop"* — so that is where it goes. */
@media (max-width: 47.99rem) {
  dialog::backdrop { backdrop-filter: none; -webkit-backdrop-filter: none; background: var(--overlay); }
}

/* ---- Every sheet scrolls, and its close button stays put -------------------
   The owner's report: *"when I click outside of a modal it should be closed or at
   least make the X button to close sticky if it's risky to lose the modal by
   clicking outside of it so user could use the close button without scrolling
   back up."*

   Both, in fact, because they answer different risks. Click-outside is handled in
   `dialog.js` and is refused for a sheet that would lose typed input. The sticky
   header is here, and applies to all of them — a long settings list should not
   require scrolling back to the top to leave. */
/* `display` on `[open]` only — the bare selector must never set it, or a closed
   dialog beats the browser's own `display: none` and sits on the page as an
   interstitial. `tests/test_css_contracts.py` enforces that; this is why the height
   cap and the flex column are split across two rules rather than one. */
.sheet { max-block-size: 88vh; }
.sheet[open] { display: flex; flex-direction: column; }
.sheet__body {
  /* No padding above: the sticky header owns that space (see `.sheet__title`).
     The bottom keeps a generous pad plus the phone's safe area, or the last row
     sits under the home indicator and reads as half-cut. */
  padding-block: 0 calc(var(--space-6) + env(safe-area-inset-bottom, 0px));
  /* `min-block-size: 0` is the whole reason this scrolls.
     -------------------------------------------------------------------------
     A flex item's default `min-height: auto` refuses to shrink below its content,
     so `overflow-y: auto` on it never has anything to overflow — the item grows,
     the sheet grows past its own `max-block-size`, and the list runs off the
     bottom of the screen with no scrollbar. That was the reported bug: the
     settings and insights sheets both scrolled the *page* behind them instead of
     their own contents. It is the single most common flexbox trap and it is worth
     the comment. */
  flex: 1 1 auto;
  min-block-size: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.sheet__title {
  /* The gap above the header, and why it kept coming back.
     -------------------------------------------------------------------------
     A sticky element pins to the **padding edge** of its scroll container, not the
     border edge. So while `.sheet__body` had `padding-block-start`, the header
     stopped one padding's-worth below the top of the scrollport and content slid
     up through the strip above it. Negative margins moved the header's box but
     changed nothing about where it *sticks*, which is why pulling it up did not
     fix it.

     The fix is to leave no padding above it at all: the body starts flush, the
     header carries its own padding, and it sticks at 0 with nothing behind it. */
  position: sticky; inset-block-start: 0; z-index: 2;
  margin-inline: calc(-1 * var(--space-5));
  margin-block: 0 var(--space-4);
  padding: var(--space-4) var(--space-5) var(--space-3);
  /* An explicit colour, not `inherit`. `background: inherit` on a sticky element
     resolves to the *parent's* background, which for a plain sheet is
     `transparent` — so rows scrolled up and straight through the header. */
  background: var(--surface);
  box-shadow: 0 1px 0 var(--hairline);
}
.sheet--settings .sheet__title { background: var(--page-tint); }

/* ---- Scrollbars, inside our surfaces only ---------------------------------
   The owner's line: *"the scroll bar default design on the browser should be kept
   but all modals scroll bars might need improvement cause that's a ui inside our
   website and app."* Exactly right, and the distinction is the whole rule — the
   page's scrollbar belongs to the browser and to the person's own settings; a
   scrollbar *inside* a sheet is a piece of our interface.

   Scoped to scrolling surfaces we drew. `html`/`body` are deliberately untouched. */
.sheet__body,
.usermenu__panel,
.panel__body {
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}
.sheet__body::-webkit-scrollbar,
.usermenu__panel::-webkit-scrollbar,
.panel__body::-webkit-scrollbar { inline-size: 10px; block-size: 10px; }
.sheet__body::-webkit-scrollbar-track,
.usermenu__panel::-webkit-scrollbar-track,
.panel__body::-webkit-scrollbar-track { background: transparent; }
.sheet__body::-webkit-scrollbar-thumb,
.usermenu__panel::-webkit-scrollbar-thumb,
.panel__body::-webkit-scrollbar-thumb {
  background: var(--border-strong);
  border-radius: var(--radius-pill);
  /* A transparent border with background-clip is how a thumb gets inset from its
     track without the track needing a colour of its own. */
  border: 3px solid transparent;
  background-clip: content-box;
}
.sheet__body::-webkit-scrollbar-thumb:hover,
.usermenu__panel::-webkit-scrollbar-thumb:hover,
.panel__body::-webkit-scrollbar-thumb:hover { background: var(--text-subtle); background-clip: content-box; }

/* ---- Sticky section tabs --------------------------------------------------
   *"still the tabs could be improved so let the user know what tab they're in. we
   can make the tabs sticky so user can switch between them"*.

   Only when there is more than one — a single tab that follows you down the page
   is a wasted bar. The `:has()` guard does that in CSS; where `:has()` is missing
   the rail simply does not stick, which is the old behaviour. */
/* Sticky was tried on the labels themselves and it was wrong.
   -----------------------------------------------------------------------------
   Every label in a flex row, each `position: sticky` at the same offset, sticks
   independently — so on scroll they all pin to the same line and slide *over each
   other*, and the trailing "+ Add a subcategory" tab landed on top of the first
   one. That is exactly what the owner reported.

   Making the rail stick as a unit needs a wrapper around the labels, and a wrapper
   is the one thing this component cannot have: `input:checked + label + panel` is
   what shows a section with no JavaScript, and an element between those three
   breaks the sibling chain.

   So the sticky thing is the **caption inside the panel** instead — a single
   element, which sticky handles correctly, and which answers the actual question
   ("which tab am I in?") better than a pinned rail does, because it stays legible
   while you are reading rather than only while you are at the top.

   Horizontal scrolling of the rail has the same wrapper problem and is not built.
   The rail wraps instead: every tab stays visible and reachable, which for the
   three-to-five sections a real profile has is the better trade anyway. Noted in
   `docs/product/open-threads.md` in case a profile ever grows enough tabs to
   change that. */
.tabs { position: relative; }
.tabs__label { order: 1; }
/* The `+` must be the last thing in the rail, not the first. It is an `<a>` rather
   than a label-for-a-radio, so it never matched the `order: 1` that every real tab
   gets from `.tabs__label` — and a flex item with no order defaults to 0, which put
   "add a subcategory" in front of the subcategories. */
.tabs__label--add { order: 1; }

/* The caption that used to sit here is gone — the rail sticks instead. See
   `.tabs__heading` above. Its sticky treatment lives on, in that the rail is the
   thing pinned to the top of the panel now rather than a second copy of its name. */

/* ---- Search, live -------------------------------------------------------- */
/* The inner field had a focus ring of its own inside the bar's focus ring. Two
   rings for one control reads as a bug, and it is — the bar is the control. */
.searchbar input,
.searchbar input:focus,
.searchbar input:focus-visible { border: 0; outline: none; box-shadow: none; }
.searchbar button[disabled] { opacity: 0.4; cursor: not-allowed; }
/* Results swap under the field as you type. Only the opacity moves: shifting the
   list makes the page feel like it is fighting you while you are still typing. */
.search-results { transition: opacity 120ms var(--ease); }
.search-results[data-busy] { opacity: 0.5; }
@media (prefers-reduced-motion: reduce) { .search-results { transition: none; } }

/* ---- The account menu, made readable --------------------------------------
   *"the desktop sub menu items can be a bit larger and more readable"*. */
.usermenu__panel { min-inline-size: 16rem; padding: var(--space-2); }
.usermenu__panel a,
.usermenu__panel button {
  min-block-size: 2.9rem;
  padding: var(--space-3);
  font-size: var(--step-0);
  gap: var(--space-3);
}
.usermenu__panel .icon { inline-size: 1.15rem; block-size: 1.15rem; }
.usermenu__name { font-size: var(--step-0); }

/* ---- Identity, aligned ----------------------------------------------------
   The handle sat on its own line with the heading's line-height still applied to
   the block above it, so it read as detached from the name rather than under it.
   An explicit line-height and a hair of space is all it needed. */
.identity__who { display: grid; gap: 2px; align-content: center; }
.identity__name { line-height: 1.2; }
.identity__handle { line-height: 1.2; margin: 0; }
/* Same fix in the account menu, where the two lines had the same problem. */
.usermenu__head { display: grid; gap: 2px; }
.usermenu__name, .usermenu__handle { line-height: 1.25; }

/* ---- Page heads and list rows ---------------------------------------------
   Every owner-facing page had its own composition: a `.hero` here, a `.card`
   wrapping a `<ul class="payment-list">` there, a `.tree` somewhere else. They
   were built at different times and it showed.

   The owner's instruction after seeing the settings sheet: *"I wish all parts of
   our system would be elegant and minimal like this."* So the sheet's vocabulary —
   a quiet title, grouped inset lists, rows with a trailing value — becomes the
   vocabulary for every page, and these are the two pieces it was missing. */
.page-head {
  display: grid; gap: var(--space-1);
  padding-block: var(--space-6) var(--space-4);
}
.page-head__eyebrow {
  font-size: var(--step--1); font-weight: 600; color: var(--text-subtle);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.page-head h1 { margin: 0; font-size: var(--step-2); letter-spacing: -0.02em; }
.page-head__lead { margin: 0; color: var(--text-muted); max-inline-size: 60ch; }
.page-head__actions { display: flex; gap: var(--space-2); flex-wrap: wrap; margin-block-start: var(--space-2); }

/* A list row that is a *destination*, not a table cell: a title, a line of
   context under it, and a trailing state. The whole row is the target, because a
   row where only the four-letter word "Open" is tappable is a row that is hard to
   tap. */
.list-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center; gap: var(--space-1) var(--space-3);
  padding: var(--space-3) var(--space-4);
  color: var(--text); text-decoration: none;
}
.list-row + .list-row { box-shadow: inset 0 1px 0 var(--hairline); }
.list-row:hover { background: var(--surface-2); }
.list-row__mark {
  grid-row: span 2; align-self: center;
  inline-size: 0.5rem; block-size: 0.5rem; border-radius: 50%;
  background: transparent; flex: none;
}
/* Unread is a dot, not a badge that says "New". The dot is the convention every
   mail and message client already taught, it needs no translation, and it does not
   push the title along the row. */
.list-row--unseen .list-row__mark { background: var(--accent); }
.list-row--unseen .list-row__title { font-weight: 650; }
.list-row__title { min-inline-size: 0; overflow-wrap: anywhere; }
.list-row__meta {
  grid-column: 2; font-size: var(--step--1); color: var(--text-subtle);
  display: flex; flex-wrap: wrap; gap: 0 var(--space-2);
}
.list-row__meta > * + *::before { content: "·"; margin-inline-end: var(--space-2); }
.list-row__trail {
  grid-row: span 2; align-self: center;
  display: flex; align-items: center; gap: var(--space-2);
  font-size: var(--step--1); color: var(--text-subtle); white-space: nowrap;
}
.list-row__go { inline-size: 1rem; block-size: 1rem; opacity: 0.5; flex: none; }
/* A row that *is* the action — a phone number to dial, a form with one button.
   Same geometry as a plain row so the group stays a single ruled list. */
.list-row--action { cursor: pointer; }
.list-row--action .list-row__trail { color: var(--accent); font-weight: 600; }
/* A row whose content is a paragraph or an image rather than a label and a value:
   it drops the three-column grid and takes the full width. */
.list-row--block, .list-row--media { grid-template-columns: minmax(0, 1fr); gap: var(--space-2); }
.list-row--block .list-row__body { margin: 0; overflow-wrap: anywhere; }
/* One request, as a stack of grouped lists — the settings-sheet vocabulary, because
   this screen is read standing up and the eye needs one thing per line. */
.lead-detail { display: grid; gap: var(--space-4); max-inline-size: 44rem; margin-inline: auto; }
.group--padded { padding: var(--space-4); }
[dir="rtl"] .list-row__go { transform: scale(-1, 1); }

/* An empty screen is an invitation to act, never a shrug. Every one of these
   states names the thing that is missing and gives the control that creates it. */
.empty {
  display: grid; justify-items: center; gap: var(--space-3);
  padding: var(--space-8) var(--space-5);
  text-align: center;
  background: var(--surface); border-radius: var(--radius);
}
.empty__icon {
  display: grid; place-items: center;
  inline-size: 3rem; block-size: 3rem; border-radius: 50%;
  background: var(--accent-soft); color: var(--accent);
}
.empty h2 { margin: 0; font-size: var(--step-1); }
.empty p { margin: 0; color: var(--text-muted); max-inline-size: 44ch; }

/* Filter chips, shared by the requests inbox and the reports range picker. */
.chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.chip {
  display: inline-flex; align-items: center; gap: var(--space-2);
  min-block-size: 2.25rem; padding-inline: var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--surface-2); color: var(--text-muted);
  font-size: var(--step--1); font-weight: 600; text-decoration: none;
}
.chip:hover { background: var(--accent-soft); color: var(--accent); }
.chip--active { background: var(--accent); color: var(--on-accent); }
.chip__count { font-variant-numeric: tabular-nums; opacity: 0.75; }

/* Status chips on a received request. The same pill, doing three different jobs, so
   the row reads as one control rather than as a mixture of labels and buttons.

   `--current` is where this request is now; `--action` is a move that can be made
   from here; `--spent` is a state on the path that this request can no longer reach.
   Spent chips stay visible on purpose — a control that loses members as the request
   moves is one nobody learns the shape of. */
.chip--current { background: var(--accent); color: var(--on-accent); cursor: default; }
.chip--current.badge--success { background: var(--success); color: #fff; }
.chip--current.badge--danger { background: var(--danger); color: #fff; }
.chip--action { border: 1px solid var(--border); background: var(--surface); cursor: pointer; font: inherit; font-size: var(--step--1); font-weight: 600; }
.chip--action:hover { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
.chip--action.badge--danger:hover { border-color: var(--danger); background: var(--danger-soft); color: var(--danger); }
.chip--spent { background: transparent; color: var(--text-subtle); opacity: 0.55; cursor: default; }
.chip--spent:hover { background: transparent; color: var(--text-subtle); }
/* Each action chip is its own form, so the flex row has to see through the wrapper. */
.chips > form { display: contents; }

/* A stat tile — the reports page's totals, and the balance page's headline. */
.tiles {
  display: grid; gap: var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
}
.tile {
  display: grid; gap: var(--space-1);
  padding: var(--space-4);
  border-radius: var(--radius); background: var(--surface);
}
.tile__value {
  font-size: var(--step-2); font-weight: 700; letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums; line-height: 1.1;
}
.tile__label { font-size: var(--step--1); color: var(--text-subtle); }
.tile--money .tile__value { color: var(--gold); }

/* ---- The directory, brought up to the same finish --------------------------
   The result cards were the oldest surface in the product and read that way:
   heavier shadows, a tighter grid and a sort control built from text links, all
   from before the rest of the interface settled on inset groups and pill chips.

   The markup is deliberately untouched — those cards carry the SEO-relevant
   structure and several tests assert their classes. This is finish, not surgery. */
.pcard {
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  border: 1px solid transparent;
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
.pcard:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow);
  border-color: var(--border);
}
.pcard__head { align-items: center; gap: var(--space-3); }
.pcard__title h3 { margin: 0; font-size: var(--step-0); letter-spacing: -0.01em; line-height: 1.25; }
.pcard__niche { font-size: var(--step--1); color: var(--text-subtle); }
.pcard__snippet { font-size: var(--step--1); line-height: 1.55; }

/* The results bar: a count that reads as a sentence, and sort as chips — the same
   control the requests inbox and the reports range picker already use. Three
   different-looking ways to say "filter this list" was two too many. */
.results-bar {
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between; gap: var(--space-3);
  margin-block-end: var(--space-4);
}
.results-count { margin: 0; font-size: var(--step--1); color: var(--text-muted); }
.sort { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.sort__label {
  font-size: var(--step--2); font-weight: 600; color: var(--text-subtle);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.sort__option {
  display: inline-flex; align-items: center;
  min-block-size: 2rem; padding-inline: var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--surface-2); color: var(--text-muted);
  font-size: var(--step--1); font-weight: 600; text-decoration: none;
}
.sort__option:hover { background: var(--accent-soft); color: var(--accent); }
.sort__option--active { background: var(--accent); color: var(--on-accent); }

@media (prefers-reduced-motion: reduce) {
  .pcard { transition: none; }
  .pcard:hover { transform: none; }
}

/* ---- Filter rows ----------------------------------------------------------
   Native `<select>` elements, deliberately. iOS and macOS both render their own
   picker for one — a wheel on a phone, a menu on a desktop — which is the control
   the owner asked to match, and which no amount of custom markup imitates
   convincingly. It is also the only version that is keyboard- and
   screen-reader-correct without a thousand lines of ARIA.

   What is styled is the *frame* around it: a labelled, rounded field that reads as
   part of this interface rather than as a raw form control. */
.filters {
  display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: flex-end;
}
.filter { display: grid; gap: var(--space-1); min-inline-size: 9rem; flex: 1 1 9rem; }
.filter__label {
  font-size: var(--step--2); font-weight: 600; color: var(--text-subtle);
  text-transform: uppercase; letter-spacing: 0.06em;
  padding-inline-start: var(--space-1);
}
.filter__select {
  appearance: none;
  inline-size: 100%; min-block-size: 2.5rem;
  padding-inline: var(--space-3) var(--space-6);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text);
  font: inherit; font-size: var(--step--1); font-weight: 600;
  cursor: pointer;
  /* The chevron, drawn rather than shipped: an inline SVG here costs no request and
     cannot be missing. `background-position` is physical, so RTL gets its own rule. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236e6e73' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-size: 1rem;
  background-position: right var(--space-3) center;
}
[dir="rtl"] .filter__select {
  padding-inline: var(--space-3) var(--space-6);
  background-position: left var(--space-3) center;
}
.filter__select:hover { border-color: var(--border-strong); }
.filter__select:focus-visible { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
.filters__apply { flex: 0 0 auto; }

/* Anything swapping its contents in place fades rather than moves. Shifting a list
   while somebody is still choosing makes the page feel like it is fighting them. */
[data-live-target] { transition: opacity 120ms var(--ease); }
[data-live-target][data-busy] { opacity: 0.5; }
@media (prefers-reduced-motion: reduce) { [data-live-target] { transition: none; } }

/* ---- Lightbox -------------------------------------------------------------
   A picture on black, and nothing else. Apple's own image viewers put the frame
   entirely out of the way and let the photograph be the interface; every control
   that is not "close" is a control competing with the thing you opened.

   The image is a plain `<img>` in a scrollable figure, which is what makes pinch
   and double-tap zoom work: the browser's own zoom applies, and a hand-written one
   is always worse than the platform's. */
.lightbox {
  inline-size: 100%; max-inline-size: 100%;
  block-size: 100%; max-block-size: 100%;
  margin: 0; padding: 0; border: 0;
  background: transparent;
}
.lightbox[open] { display: grid; grid-template-rows: 1fr auto; place-items: center; }
.lightbox::backdrop {
  background: rgba(0, 0, 0, 0.92);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
/* The figure is the *scroll box*, so it must be given a real size to fit inside.
   `block-size: 100%` of a grid row that is itself `1fr` of a 100%-height dialog is
   circular on some engines and resolved to the image's natural height on others —
   which is why a large photo overflowed the window instead of fitting it. `min-*: 0`
   lets the grid row actually shrink, and the figure is then bounded by the row. */
.lightbox__figure {
  grid-row: 1;
  display: grid; place-items: center;
  margin: 0; padding: var(--space-4);
  inline-size: 100%;
  min-inline-size: 0; min-block-size: 0;
  block-size: auto;
  max-block-size: 100%;
  overflow: auto;
}
.lightbox__image {
  /* Bounded by the viewport, not by the parent: the parent's height depends on
     this element, so sizing against it is the circle that let the image win. */
  max-inline-size: calc(100vw - 2 * var(--space-6));
  max-block-size: calc(100vh - 8rem);
  inline-size: auto; block-size: auto;
  object-fit: contain;
  border-radius: var(--radius-sm);
}
.lightbox__caption {
  grid-row: 2;
  padding: var(--space-3) var(--space-5) var(--space-6);
  color: rgba(255, 255, 255, 0.85); font-size: var(--step--1); text-align: center;
}
.lightbox__close {
  position: fixed;
  inset-block-start: calc(var(--space-4) + env(safe-area-inset-top, 0px));
  inset-inline-end: var(--space-4);
  z-index: 2;
  background: rgba(255, 255, 255, 0.14); color: #fff;
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
}
.lightbox__close:hover { background: rgba(255, 255, 255, 0.24); color: #fff; }

/* The cue that a picture opens. Without it a tappable image is indistinguishable
   from a decorative one, which is how the receipts ended up looking like dead ends. */
[data-lightbox] img { cursor: zoom-in; }
[data-lightbox] .media-tile,
[data-lightbox] figure { transition: transform var(--dur) var(--ease); }
[data-lightbox] .media-tile:hover { transform: scale(1.02); }
@media (prefers-reduced-motion: reduce) {
  [data-lightbox] .media-tile { transition: none; }
  [data-lightbox] .media-tile:hover { transform: none; }
}

/* A receipt, small enough to sit in a row and large enough to recognise. */
.receipt-thumb {
  display: inline-block;
  inline-size: 3rem; block-size: 3rem;
  border-radius: var(--radius-sm); overflow: hidden;
  border: 1px solid var(--border);
  background: var(--surface-2);
}
.receipt-thumb img { inline-size: 100%; block-size: 100%; object-fit: cover; }
/* On the request page the receipt is not a row ornament — it is the thing the owner
   opened the page to look at, so it gets real size before the lightbox is even
   needed. `aspect-ratio: auto` because a transfer screenshot is tall, and cropping it
   square hides the half with the amount in it. */
.receipt-thumb--lead { inline-size: min(16rem, 100%); block-size: auto; }
.receipt-thumb--lead img { block-size: auto; object-fit: contain; }

/* At the ceiling: the slot the `+` occupied still shows the count, so the rail does
   not simply lose a control with no explanation of where it went. */
.tabs__label--full {
  order: 1;
  background: transparent; border: 1px dashed var(--border);
  color: var(--text-subtle); font-variant-numeric: tabular-nums; cursor: default;
}
.pane-head__action.help { margin-inline-start: auto; }

/* The settings hero: avatar and name side by side, both centred, state underneath.
   -----------------------------------------------------------------------------
   It was a three-column grid with the badge pinned to the trailing corner, so the
   badge floated level with the avatar while the name sat off to one side —
   *"published is centered, but other items in hero section are not."* Now the
   avatar and the text are one centred pair and the badge is its own row below
   them, which is where a piece of state belongs: after the thing it describes. */
.page-head--identity {
  grid-template-columns: auto auto;
  grid-template-areas: "avatar who" "badge badge";
  justify-content: center;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  text-align: start;
}
.page-head--identity > .avatar { grid-area: avatar; }
.page-head--identity .identity__who { grid-area: who; min-inline-size: 0; }
.page-head--identity h1 { font-size: var(--step-2); }
.page-head__badge {
  grid-area: badge;
  justify-self: center;
  margin-block-start: var(--space-2);
}

/* The bell's popover. Positioned against the icon rather than the viewport, so it
   works in the header and in the profile hero without either knowing about it. */
.notify-pop-host { position: relative; }
.notify-pop {
  position: absolute; inset-block-start: var(--space-2); inset-inline-end: 0;
  z-index: 60;
  inline-size: 22rem; max-inline-size: calc(100vw - 2 * var(--gutter));
  max-block-size: 70vh; overflow-y: auto;
}
.notify-pop .list-row { padding-inline: var(--space-3); }
/* Title on one side, "See all" on the other. The head is a grid everywhere else,
   so this overrides only the axis — and uses `justify-content: space-between` with
   logical columns rather than a float, so RTL needs no second rule. */
.notify-pop__head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-3);
}
.notify-pop__all { font-size: var(--step--1); font-weight: 500; white-space: nowrap; }
.notify-pop__readall { padding: 0 var(--space-3) var(--space-2); }

/* ---- Editing controls, made legible ---------------------------------------
   The grip is the one control here you drag rather than tap, so it is the largest
   thing in the row instead of the smallest. */
.edit-tools__grip .icon { inline-size: 1.35rem; block-size: 1.35rem; }
.edit-tools__grip { padding-inline: var(--space-1); }
@media (pointer: coarse) {
  /* A finger, not a cursor. Worth the extra few pixels on the only target here
     that has to be acquired and held rather than hit once. */
  .edit-tools__grip .icon { inline-size: 1.6rem; block-size: 1.6rem; }
}

/* Red while the thing is still visible: pressing this takes it off the public page.
   Once hidden the control goes quiet, so the two states differ by colour as well as
   by glyph rather than being the same grey icon twice. */
.edit-tools__btn--hide { color: var(--danger); }
.edit-tools__btn--hide:hover { background: var(--danger-soft); border-color: var(--danger); }
.edit-tools__btn--off { color: var(--text-subtle); }

/* ---- Auth, trimmed --------------------------------------------------------
   An eyebrow, a display heading and a lead paragraph is three pieces of furniture
   in front of two fields. On a phone the thing you came to do started below the
   fold. The heading drops a step and the lead becomes help text. */
.auth__head { text-align: center; margin-block-end: var(--space-4); }
.auth__head h1 { font-size: var(--step-2); margin: 0 0 var(--space-1); }
.auth__head .help { margin: 0; }

/* ---- Password rules -------------------------------------------------------
   The instruction and the validator in one control. Stated before anything is
   typed, because "too common" after a page reload tells somebody they are wrong
   without telling them what right looks like. */
.pwrules { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-1); }
.pwrule {
  display: flex; align-items: center; gap: var(--space-2);
  font-size: var(--step--1); color: var(--text-subtle);
  transition: color var(--dur) var(--ease);
}
.pwrule::before {
  content: ""; flex: none;
  inline-size: 1rem; block-size: 1rem;
  border-radius: 50%;
  border: 1.5px solid var(--border-strong);
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.pwrule.is-met { color: var(--success); }
.pwrule.is-met::before {
  background: var(--success) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6L9 17l-5-5'/%3E%3C/svg%3E") center / 0.7rem no-repeat;
  border-color: var(--success);
}
@media (prefers-reduced-motion: reduce) { .pwrule, .pwrule::before { transition: none; } }

/* ---- A profile over the directory (B3) ------------------------------------
   Wider and taller than a settings sheet: this holds a whole profile, and the
   point of it is to read the profile without losing the list underneath. */
.sheet--profile {
  /* `inset: 0` **and** `margin: auto` — the pair is what centres a dialog.
     -------------------------------------------------------------------------
     This was `inset: auto; margin: auto`, which does nothing: `.sheet` is
     `position: fixed`, and a fixed box with every inset `auto` is laid out at its
     static position with no box to centre inside. The dialog was there, with a
     backdrop, and its contents were off-screen — which is exactly what the owner
     saw: *"it changes the address bar but doesn't show anything, just a blurred
     background and no modal."*

     The bottom-sheet geometry `.sheet` sets is also cancelled here rather than
     partly overridden. This is a centred card, not a sheet rising from the bottom
     edge, and inheriting half of the other thing is how it ended up in neither
     place. */
  inset: 0;
  margin: auto;
  inline-size: min(56rem, calc(100vw - 2 * var(--space-6)));
  max-inline-size: none;
  block-size: fit-content;
  max-block-size: 88vh;
  border-radius: var(--radius-lg);
  /* Rising from the bottom is wrong for a centred card and, combined with fixed
     positioning, was pushing it further out of view. */
  animation: none;
}
.sheet--profile .sheet__body { padding-block-start: 0; }
.sheet--profile .sheet__title { justify-content: space-between; }
/* The sheet already frames the section, so the panel drops its own full-bleed
   background — two nested tinted containers read as a rendering fault. */
.sheet--profile .tabs__panel {
  margin-inline: 0; padding-inline: 0; background: transparent; box-shadow: none;
}
/* The rail stops being sticky inside the modal — and this rule is the reason it is
   worth writing down rather than deleting with the caption it used to name.

   Two sticky elements both pinned at `inset-block-start: 0` — the sheet's own header
   and whatever the panel pins — land on top of each other. That was the "sticky head
   getting mixed up with the profile tab" the owner reported when the *caption* was
   the sticky thing. The caption is gone and the tab rail took over its job, so the
   collision would have come straight back here, in a modal, one round later.

   Outside the sheet the rail is the only sticky thing in its scroller and keeps it. */
.sheet--profile .tabs__heading { position: static; }
.sheet--profile .tabs__heading::before { content: none; }
/* Not offered on a phone: a sheet holding a whole profile over a list is a desktop
   idiom, and at that width the profile page itself is the better answer — which is
   why `card-modal.js` does not intercept the tap there at all.

   The rule is on `[open]`, never the bare selector: a `display` on a closed dialog
   beats the browser's own `display: none` and the dialog renders as an interstitial.
   `tests/test_css_contracts.py` refuses it, and has caught this twice before. */
@media (max-width: 47.99rem) { .sheet--profile[open] { display: none; } }

/* =============================================================================
   App layer IV — 2026-08-04, fifth pass
   ========================================================================== */

/* ---- Call: one control, sized like one ------------------------------------
   Full width made it read as a page-level action competing with the services
   below it. It is one control, so it is the size of one, and it carries the
   number as its own label — a digit string is what people check before they
   press, and hiding it behind the word "Call" made them press to find out. */
.call {
  display: inline-flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius);
  background: var(--accent-soft); color: var(--accent);
  text-decoration: none; font-weight: 600;
  transition: background var(--dur) var(--ease);
  inline-size: fit-content;
}
.call:hover { background: color-mix(in srgb, var(--accent) 18%, transparent); }
.call__icon {
  display: grid; place-items: center; flex: none;
  inline-size: 2.25rem; block-size: 2.25rem; border-radius: 50%;
  background: var(--accent); color: var(--on-accent);
}
.call__body { display: grid; gap: 1px; }
.call__label {
  font-size: var(--step--2); text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--text-subtle);
}
.call__number { font-size: var(--step-0); font-variant-numeric: tabular-nums; }

/* Bio and call side by side, the bio taking the room. Stacks on a phone with the
   call last, which is where a thumb is. */
.bio-row { display: grid; gap: var(--space-4); align-items: start; }
@media (min-width: 48rem) {
  .bio-row { grid-template-columns: minmax(0, 3fr) minmax(0, 1fr); }
  .bio-row .call { justify-self: end; }
}

/* ---- Address, under the places it describes ------------------------------- */
.address {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: var(--space-1) var(--space-2);
  margin-block: var(--space-3) 0;
}
.address__icon { grid-row: span 3; color: var(--text-subtle); margin-block-start: 0.15em; }
.address__label {
  font-size: var(--step--2); font-weight: 600; color: var(--text-subtle);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.address__text { color: var(--text-muted); }

/* ---- Content blocks: one section, not a stack of cards ---------------------
   Each block was a `.card` with its own shadow and radius, so a page of four read
   as four unrelated objects that happened to be adjacent. The owner's note: *"each
   block looks so separate, we need to make the whole content section a bit more
   unified but each block slightly separated."*

   The Apple answer is the one already used everywhere else here: **one container,
   hairline-separated rows**. The group is the object; the blocks are its parts.
   Nothing floats, nothing casts its own shadow, and the seams say "these belong
   together and here is where one ends". */
.content-blocks {
  background: var(--surface);
  border-radius: var(--radius);
  overflow: hidden;
}
.content-blocks > .card {
  border-radius: 0;
  box-shadow: none;
  background: transparent;
  margin: 0;
  padding: var(--space-5);
}
.content-blocks > .card + .card { box-shadow: inset 0 1px 0 var(--hairline); }
/* While editing, each block keeps a little more air: there are controls in it, and
   a control strip needs room that a paragraph does not. */
.content-blocks > .editable { padding-block: var(--space-4); }

/* ---- The profile hero, centred on a wide screen ---------------------------
   On desktop the identity sat hard against the leading edge with a screen of empty
   space beside it. On a phone the gear and bell fill that space, so it stays as it
   is there. */
@media (min-width: 48rem) {
  /* The gap this fixes.
     -------------------------------------------------------------------------
     The grid reserved a `tools` column on every profile, but the gear and bell
     only exist for the owner in edit mode. For everyone else — which is everyone
     — that column was empty and `justify-items: center` pushed the avatar and the
     name apart around a hole. The owner's words: *"there's a huge gap between
     profile photo and the profile name and username."*

     So the columns are sized to their contents and the whole block is centred as a
     unit. With tools present it is avatar · text · tools; without them it is
     avatar · text, tight, and still centred. Nothing reserves space for something
     that is not there. */
  .identity {
    grid-template-columns: auto auto;
    grid-template-areas: "avatar who" "avatar meta" "metrics metrics" "actions actions";
    justify-content: center;
    justify-items: start;
    inline-size: fit-content;
    max-inline-size: 46rem;
    margin-inline: auto;
    column-gap: var(--space-5);
  }
  /* Only when there is something to put in it. `:has()` is the whole point here —
     the layout follows the markup rather than assuming it. */
  .identity:has(.identity__tools) {
    grid-template-columns: auto auto auto;
    grid-template-areas: "avatar who tools" "avatar meta tools" "metrics metrics metrics" "actions actions actions";
  }
  .identity__meta { justify-content: flex-start; }
  .identity__actions { justify-content: flex-start; }
  /* The two full-width rows must span whatever the container ended up being. */
  .metrics, .identity__actions { inline-size: 100%; }
}
/* No `:has()` — fall back to the two-column layout, which is right for the common
   case and merely a little tight for an owner. */
@supports not selector(:has(*)) {
  @media (min-width: 48rem) {
    .identity { grid-template-columns: auto minmax(0, 1fr) auto; inline-size: auto; }
  }
}
/* Bigger, as asked: the handle is how people refer to a business out loud and in
   messages, and it was set two steps below the name. */
.identity__handle { font-size: var(--step-0); }

/* ---- Settings hero, centred beside the photo ------------------------------ */
/* (Centring for this block is handled where it is defined, above. A second copy
   here previously fought it: `justify-items: center` on the grid centred each cell
   independently, which is what put the badge level with the avatar and the name
   adrift from both.) */
/* The badge moves under the name rather than floating in the trailing corner,
   where it read as unrelated chrome. */
.page-head__badge { grid-column: 1 / -1; justify-self: center; margin-block-start: var(--space-2); }

/* ---- The tab bar: a surface, not a suggestion ------------------------------
   88% of a light surface over scrolling content is not enough separation — text
   passing underneath stayed legible through it. More opacity, more blur. */
/* Opaque. Two passes at "a bit less transparent" were still see-through enough to
   read text through, so it is simply the surface colour now with the blur kept for
   the edge where content meets it. A bar you can read the page through is not a
   bar, it is a watermark. */
.tabbar {
  background: var(--surface);
  backdrop-filter: saturate(180%) blur(28px);
  -webkit-backdrop-filter: saturate(180%) blur(28px);
}
/* Where backdrop-filter is unavailable the bar must not be see-through at all. */
@supports not (backdrop-filter: blur(1px)) {
  .tabbar { background: var(--surface); }
}

/* (The rule that used to sit here reinstated `padding-block-start` on
   `.sheet__body` and was the reason the header gap survived two attempts at
   fixing it. Sticky pins to the padding edge — see `.sheet__title`.) */

/* ---- Bars ------------------------------------------------------------------
   A row of numbers makes the reader do the comparison; a bar makes the comparison
   the thing they see. Widths are a percentage of the largest row in the same
   block, computed server-side — an inline width is the one place a style belongs
   in markup, because there it is data rather than design. */
.bars { display: grid; gap: var(--space-2); }
.bar {
  display: grid;
  grid-template-columns: minmax(6rem, 1fr) minmax(4rem, 2fr) auto;
  align-items: center; gap: var(--space-3);
  font-size: var(--step--1);
}
.bar__label { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bar__track {
  block-size: 0.5rem; border-radius: var(--radius-pill);
  background: var(--surface-2); overflow: hidden;
}
.bar__fill {
  display: block; block-size: 100%;
  border-radius: var(--radius-pill);
  background: var(--accent);
  min-inline-size: 2px;
}
.bar__value {
  color: var(--text-subtle); font-variant-numeric: tabular-nums; white-space: nowrap;
}
@media (max-width: 30rem) {
  .bar { grid-template-columns: 1fr auto; }
  .bar__track { grid-column: 1 / -1; }
}

/* =============================================================================
   Eleventh pass — the owner panel joins the design system, 2026-08-12
   -----------------------------------------------------------------------------
   The owner's report: *"I like the list of settings on gear icon click on profile
   page… but not every part of the system is like the apple/ios/macbook styles and
   principles."* That is an accurate description of what was actually shipped. The
   2026-08-04 redesign built a complete iOS-grade vocabulary — `.page-head`,
   `.group` + `.group-title`, `.list-row`, `.empty`, sheets — and applied it to the
   *visitor-facing* profile, the account sheet, the dashboard and the requests inbox.
   The owner panel, which is 23 templates and the place an owner actually spends
   their time, was never converted. It was still on the pre-redesign composition:
   `.hero` heads, `.card` + `.form-grid` forms, and `.tree` — a row built for the
   category tree — pressed into service for services, menu options, questions,
   content blocks and locations.

   So this pass adds no new *look*. It adds the three idioms the existing vocabulary
   was missing, which are exactly the three things those screens need, and then the
   templates are rewritten onto it. Anything that already had an answer keeps it.

   1. **`.form-list`** — a form as a grouped inset list. iOS never shows a stack of
      label-above-input boxes; it shows rows with the label on the leading edge and
      the control on the trailing edge, ruled by hairlines, inside one card. Wide
      controls (a bio, a picker, a switch with a description) take the full row.
   2. **Edit mode** — a list row is *one* tap target that goes somewhere. Reorder
      handles and Remove buttons live behind an Arrange/Done toggle rather than
      sitting on every row for ever. Six controls per row is the single biggest
      reason these screens read as a control panel instead of an interface.
   3. **`.page-head__back`** — one back affordance, in one place, naming where it
      goes. The panel had six spellings of it ("Back to dashboard", "Back to
      services", "Back to service", "Back to your profile", "All sections", "Your
      profile"), each in a different position on the page.

   Everything here is additive: no token changes, no rules removed. `.tree` stays,
   because the category and location hierarchies still want a tree.
   ========================================================================== */

/* ---- A back affordance, in one place ---------------------------------------
   iOS puts one back control at the leading edge above the title, and it names its
   destination so you can tell where it goes without pressing it. Sitting inside
   `.page-head` means it cannot drift to the other side of the screen on the next
   screen someone builds. */
.page-head__back {
  display: inline-flex; align-items: center; gap: var(--space-1);
  align-self: start;
  margin-block-end: var(--space-1);
  color: var(--accent); text-decoration: none;
  font-size: var(--step--1); font-weight: 600;
}
.page-head__back:hover { text-decoration: underline; }
/* The chevron points back the way the language runs — one glyph, mirrored, so a
   market that ever runs LTR gets the correct arrow for free. */
.page-head__back .icon { inline-size: 1rem; block-size: 1rem; transform: scale(-1, 1); }
[dir="rtl"] .page-head__back .icon { transform: none; }

/* ---- A form as a grouped inset list ---------------------------------------
   The same object as `.group`, with `.field`s for rows. Two columns on anything
   wider than a phone: the label on the leading edge, the control on the trailing
   edge, aligned down the list so the eye reads one column of questions and one of
   answers. On a phone it stacks, because a 12rem label column leaves nothing for
   the input.

   `.field--wide` (a bio, a rich-text body, a tree picker, a switch with an
   explanation) keeps the label above and takes the whole row. That distinction
   already existed in `_field.html`; this is the first layout that uses it. */
.form-list {
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  display: grid;
}
.form-list > .field,
.form-list > .form-disclosure { padding: var(--space-3) var(--space-4); }
.form-list > * + * { box-shadow: inset 0 1px 0 var(--hairline); }
.form-list > *:first-child {
  border-start-start-radius: var(--radius); border-start-end-radius: var(--radius);
}
.form-list > *:last-child {
  border-end-start-radius: var(--radius); border-end-end-radius: var(--radius);
}
@media (min-width: 34rem) {
  .form-list > .field {
    display: grid;
    grid-template-columns: minmax(7rem, 13rem) minmax(0, 1fr);
    align-items: baseline;
    gap: var(--space-1) var(--space-4);
  }
  .form-list > .field > .label { margin: 0; padding-block-start: 0.55rem; }
  /* Help and errors sit under the control, not under the label — they describe
     what you type, and a hint stranded in the label column reads as part of the
     question. */
  .form-list > .field > .help,
  .form-list > .field > .field__error { grid-column: 2; }
  .form-list > .field--wide { grid-template-columns: minmax(0, 1fr); }
  .form-list > .field--wide > .help,
  .form-list > .field--wide > .field__error { grid-column: 1; }
}
/* A switch is a row, not a field: label leading, control trailing, on one line,
   exactly like every other settings row on the site. */
.form-list > .field--switch { grid-column: auto; }
/* `row-reverse` reverses along the *inline* axis, so the toggle lands on the
   trailing edge in an RTL market as well — which is where iOS puts it, and where
   the eye looks for the answer once it has read the question. */
.form-list .switch { flex-direction: row-reverse; }
.form-list .switch__body { flex: 1; min-inline-size: 0; }

/* The form's buttons, under the card rather than inside it — the card is the
   thing being edited, the button is what you do with it. */
.form-actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3);
  margin-block-start: var(--space-4);
}
/* One column of content, centred, at a width a form is comfortable to read at.
   Replaces the `style="max-width: 44rem; margin-inline: auto"` that was copied
   into nine templates. */
.pane { max-inline-size: 46rem; margin-inline: auto; display: grid; gap: var(--space-4); }

/* ---- Edit mode: a list row is one tap target ------------------------------
   Every owner list carried its controls permanently: a grip, Move up, Move down,
   Edit, Remove and sometimes a switch — six targets on a row whose *content* was
   two lines of text. On a phone the content lost.

   iOS's answer, and it is the right one: the row navigates, and everything that
   rearranges or destroys lives behind an Edit button. Here that is a hidden
   checkbox and a `<label>`, so it works with **no JavaScript at all** — the same
   mechanism the nav menu uses, and for the same reason.

   The checkbox has to precede the rows in source order for `~` to reach them. */
.listing { display: grid; gap: var(--space-2); }
.listing__toggle { position: absolute; opacity: 0; pointer-events: none; }
.listing__head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-3);
  padding-inline: var(--space-4);
}
.listing__head .group-title { padding-inline: 0; margin: 0; }
/* The toggle looks like the quiet text button iOS uses, not like a `.btn`. */
.listing__edit {
  color: var(--accent); font-size: var(--step--1); font-weight: 600;
  cursor: pointer; padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm); white-space: nowrap;
}
.listing__edit:hover { background: var(--accent-soft); }
.listing__toggle:focus-visible ~ .listing__head .listing__edit { outline: none; box-shadow: var(--ring); }
.listing__edit-done { display: none; }
.listing__toggle:checked ~ .listing__head .listing__edit-arrange { display: none; }
.listing__toggle:checked ~ .listing__head .listing__edit-done { display: inline; }

/* Hidden until asked. `visibility` rather than `display` inside a grid row would
   still reserve the column, so these are removed outright and the row reflows. */
.list-row__tools { display: none; }
.listing__toggle:checked ~ * .list-row__tools {
  display: flex; align-items: center; gap: var(--space-1);
}
/* In edit mode the chevron is meaningless — the row no longer navigates, it is
   being rearranged. */
.listing__toggle:checked ~ * .list-row__go { display: none; }
.listing__toggle:checked ~ * .list-row { cursor: default; }

/* Reorder controls appear only once the script has made the list draggable, so
   the interface never offers a handle that does nothing. Remove has no such
   dependency and is always in edit mode. */
.list-row__tools .list-row__move { display: none; }
.is-reorderable .list-row__tools .list-row__move { display: inline-flex; }
/* Two classes, not one. The grip also carries `.tool-btn`, which sets
   `display: inline-grid` — equal specificity and defined earlier in this file, so a
   single-class `.list-row__grip { display: none }` loses the cascade and the handle
   is on the screen permanently, which is the exact thing edit mode exists to stop.
   The reveal below is three classes deep and beats both. */
.list-row .list-row__grip { display: none; }
.listing__toggle:checked ~ * .is-reorderable .list-row__grip,
.is-reorderable .listing__toggle:checked ~ * .list-row__grip { display: flex; }

/* A round icon button sized for a thumb, which is what these tools are made of. */
.tool-btn {
  display: inline-grid; place-items: center; flex: none;
  inline-size: 2.25rem; block-size: 2.25rem;
  border: 0; border-radius: 50%;
  background: var(--surface-2); color: var(--text-muted);
  cursor: pointer; padding: 0; font: inherit;
}
.tool-btn:hover { background: var(--accent-soft); color: var(--accent); }
.tool-btn:focus-visible { outline: none; box-shadow: var(--ring); }
.tool-btn .icon { inline-size: 1.05rem; block-size: 1.05rem; }
.tool-btn--danger:hover { background: var(--danger-soft); color: var(--danger); }
.tool-btn--grip { cursor: grab; background: none; }
.tool-btn--grip:hover { background: var(--surface-2); color: var(--text-muted); }
/* Each tool is its own form when it posts; the flex row has to see through it. */
.list-row__tools > form { display: contents; }

/* A row that is being dragged. */
.list-row.is-dragging { opacity: 0.4; }

/* ---- A list row, extended for the owner panel -----------------------------
   Three additions, all of them things the panel's rows carry that a notification
   row never did: a leading grip, a state pill on the trailing edge, and a row
   that is off. */
.list-row__state {
  font-size: var(--step--2); font-weight: 600;
  padding: 0.15rem var(--space-2); border-radius: var(--radius-pill);
  background: var(--surface-2); color: var(--text-subtle);
  white-space: nowrap;
}
.list-row__state--live { background: var(--success-soft); color: var(--success); }
.list-row__state--warn { background: var(--warning-soft); color: var(--warning); }
/* An inactive row is dimmed rather than hidden or badged twice — it is still your
   content, it is simply not being shown to anyone. */
.list-row--off .list-row__title,
.list-row--off .list-row__meta { opacity: 0.55; }

/* ---- Add: the last row of the list, never a floating button ---------------
   "Add a service" was a `.btn--secondary` sitting outside the card, which made it
   read as unrelated to the list it appends to. As the final row of the group it
   is where the eye already is when it finishes reading, and it inherits the row
   geometry so the list has one shape all the way down. */
.list-add {
  display: flex; align-items: center; gap: var(--space-3);
  min-block-size: 3rem; padding: var(--space-3) var(--space-4);
  color: var(--accent); font-weight: 600; text-decoration: none;
  background: none; border: 0; inline-size: 100%; font-size: inherit;
  text-align: start; cursor: pointer;
}
.list-add:hover { background: var(--surface-2); }
.list-add__icon {
  display: grid; place-items: center; flex: none;
  inline-size: 1.75rem; block-size: 1.75rem; border-radius: 7px;
  background: var(--accent); color: var(--on-accent);
}
.list-add__icon .icon { inline-size: 1rem; block-size: 1rem; }

/* ---- The builder, as three grouped lists ----------------------------------
   The three containers keep their numbers — the owner's design, and the schedule
   screen counts in the same voice — but the box becomes a `.group` like every
   other card on the site, and the head becomes a settings-style row: number,
   title, and the switch on the trailing edge. */
.builder-step { display: grid; gap: var(--space-2); }
.builder-step__head {
  display: flex; align-items: center; gap: var(--space-3);
  padding-inline: var(--space-4);
}
.builder-step__title {
  display: flex; align-items: center; gap: var(--space-2);
  margin: 0; font-size: var(--step--1); font-weight: 600;
  color: var(--text-subtle); text-transform: uppercase; letter-spacing: 0.06em;
}
.builder-step__switch { margin-inline-start: auto; }
.builder-step__hint {
  margin: 0; padding-inline: var(--space-4);
  font-size: var(--step--1); color: var(--text-muted);
}
/* A step that is switched off keeps its head at full strength — the switch is how
   you turn it back on — and fades only what it contains. */
.builder-step--off .builder-step__hint { opacity: 0.6; }

/* ---- A row that navigates *and* carries controls ---------------------------
   The row has to be one big tap target and still hold a Remove button, and an
   `<a>` cannot contain a `<button>` — nesting interactive elements is invalid and
   the inner control stops working. So the row is a container, the title holds the
   link, and the link's `::after` is stretched over the whole row. Controls sit
   above it on the z-axis and stay clickable.

   In edit mode the stretch is withdrawn: the row is being rearranged, not opened,
   and a click that navigates away mid-edit is the most annoying bug this pattern
   can have. */
.list-row--linked { position: relative; }
.list-row__link { color: inherit; text-decoration: none; }
.list-row__link::after { content: ""; position: absolute; inset: 0; }
.list-row__link:hover { text-decoration: underline; }
.list-row__tools, .list-row__switch, .list-row__grip { position: relative; z-index: 1; }
.listing__toggle:checked ~ * .list-row__link::after { content: none; }
.listing__toggle:checked ~ * .list-row__link:hover { text-decoration: none; }
/* The grip takes the leading column the unread dot uses on a notification row —
   they never appear together, and one column keeps every row on the site aligned. */
.list-row__grip { grid-row: span 2; align-self: center; }

/* =============================================================================
   Twelfth pass — materials, 2026-08-12
   -----------------------------------------------------------------------------
   The owner: *"there are stuff that makes me think we're far from the apple style
   for ios/macbook. everything is just white… we can't just find a specific thing
   and change it and think we're done with that."*

   Right on both counts. The eleventh pass fixed *compositions* — which parts a
   screen is assembled from. This one fixes the **materials**: what those parts are
   made of. They are different problems and the second was the one being felt.

   Three changes in the token layer above do most of the work:

   1. **The page is tinted.** `--bg` was `#fbfbfd` against a `#ffffff` card. Apple
      separates a card from its page with *contrast*, not with a shadow — iOS
      Settings has no shadow anywhere in it.
   2. **A system palette.** Twelve named colours, for tinted glyph tiles. This is
      the thing that makes an iOS list scannable before it is read.
   3. **Fills and a fourth label level**, so a control's background and a piece of
      text that must not be read both have somewhere to come from.

   What follows applies them.
   ========================================================================== */

/* ---- Cards stop pretending to float ----------------------------------------
   Now that the page is tinted, the shadow is doing nothing except muddying the
   card's edge. Removing it is not a subtraction: the separation got *stronger*,
   because a 5% tint difference reads where a 1.5% one plus a blur did not. */
.group, .form-list, .schedule-settings { box-shadow: none; }
/* `--page-tint` predates `--bg` carrying this job and is still referenced by the
   settings sheet. Same value by construction now, so the two cannot drift. */
:root { --page-tint: var(--bg); }

/* ---- The tinted glyph tile -------------------------------------------------
   One component, one custom property. A row declares what kind of thing it is by
   setting `--tint`, and everything else follows.

   Written as `.row-icon` because it is no longer only a settings row's — the same
   tile marks an empty state, a section heading and a directory facet. The old
   `.settings-row__icon` name is kept as an alias below rather than renamed across
   nine templates in a pass that is already large. */
.row-icon {
  display: grid; place-items: center; flex: none;
  inline-size: 1.75rem; block-size: 1.75rem;
  border-radius: 7px;
  background: var(--tint, var(--sys-gray)); color: #fff;
}
.row-icon .icon { inline-size: 1rem; block-size: 1rem; }
/* Bigger, for an empty state or a step header, where the tile is the illustration
   rather than an index mark. `--radius-sm` keeps the squircle proportion at size. */
.row-icon--lg { inline-size: 3rem; block-size: 3rem; border-radius: var(--radius-sm); }
.row-icon--lg .icon { inline-size: 1.5rem; block-size: 1.5rem; }
/* No tile at all — the glyph on its own, in the row's own ink.
   For a list where the kind of thing is already settled and the colour would only
   compete with the list's one real action. The owner, on the builder's form list:
   *"you can use not colorful icons so the add to form button would stay visible
   enough."* */
.row-icon--plain {
  background: none; color: var(--text-subtle);
  inline-size: 1.5rem; block-size: 1.5rem;
}
.row-icon--plain .icon { inline-size: 1.25rem; block-size: 1.25rem; }
/* A tile that is the *tint over a wash* rather than solid: quieter, for a heading
   or a state that should not compete with the rows under it. */
.row-icon--soft {
  background: color-mix(in srgb, var(--tint, var(--sys-gray)) 16%, transparent);
  color: var(--tint, var(--sys-gray));
}

/* The kinds of thing this product has. A colour belongs to a kind, permanently —
   services are blue wherever services appear. Adding a row type means adding a
   line here, never a colour at the call site. */
.tint-services   { --tint: var(--sys-blue); }
.tint-sections   { --tint: var(--sys-indigo); }
.tint-content    { --tint: var(--sys-orange); }
.tint-locations  { --tint: var(--sys-green); }
.tint-calendar   { --tint: var(--sys-red); }
.tint-requests   { --tint: var(--sys-teal); }
.tint-money      { --tint: var(--gold); }
.tint-alerts     { --tint: var(--sys-pink); }
.tint-photos     { --tint: var(--sys-purple); }
.tint-reviews    { --tint: var(--sys-yellow); }
.tint-account    { --tint: var(--sys-gray); }
.tint-danger     { --tint: var(--danger); }
.tint-insights   { --tint: var(--sys-mint); }
.tint-profile    { --tint: var(--accent); }

/* The settings sheet's rows adopt the shared tile. Its own modifiers stay, so the
   existing markup keeps working while templates move over. */
.settings-row__icon {
  display: grid; place-items: center; flex: none;
  inline-size: 1.75rem; block-size: 1.75rem; border-radius: 7px;
  background: var(--tint, var(--accent)); color: #fff;
}
.settings-row--money .settings-row__icon { --tint: var(--gold); background: var(--tint); }
.settings-row--alert .settings-row__icon { --tint: var(--sys-pink); background: var(--tint); }
.settings-row--danger .settings-row__icon { --tint: var(--danger); background: var(--tint); }

/* ---- A row's leading tile -------------------------------------------------
   `.list-row` puts the grip or the unread dot in its first column. A tile goes in
   the same one — they never appear together, and one column keeps every list on
   the site aligned down the same edge. */
.list-row__icon { grid-row: span 2; align-self: center; }

/* ---- Controls sit on a fill, not on a second surface -----------------------
   `--surface-2` was doing this job as a solid, which meant every hover state had
   to know what it was sitting on. A translucent fill does not. */
.tool-btn { background: var(--fill-4); }
.tool-btn:hover { background: var(--fill-3); color: var(--accent); }
.tool-btn--grip { background: none; }
.tool-btn--grip:hover { background: var(--fill-4); color: var(--text-muted); }
.list-row__state { background: var(--fill-4); }

/* ---- The empty state, with a tile ------------------------------------------
   The circle was `--accent-soft` on every screen, so eleven empty states looked
   like the same screen. They take the tint of the thing that is missing. */
.empty__icon {
  inline-size: 3.25rem; block-size: 3.25rem; border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--tint, var(--accent)) 14%, transparent);
  color: var(--tint, var(--accent));
}
.empty__icon .icon { inline-size: 1.6rem; block-size: 1.6rem; }

/* ---- Quaternary text has a home now ---------------------------------------- */
.text-faint { color: var(--text-faint); }

/* ---- Skeletons: the shape of the answer, arriving --------------------------
   Never a spinner. A spinner says "waiting" and gives the eye nothing to rest on;
   a skeleton says "arriving" and is already the shape of what lands, so nothing
   jumps when it does.

   `--fill-4` rather than a grey, because the panel it sits in may be over a white
   card or the tinted page and a translucent fill is right on both.

   The shimmer is slow on purpose — a fast one reads as an error state. And it is
   an *opacity* pulse rather than a travelling gradient: one compositor-only
   property, no repaint, and it degrades to a flat block under reduced motion
   instead of needing a second rule to stop a background-position animation. */
.skeleton {
  background: var(--fill-4);
  border-radius: var(--radius-sm);
  animation: skeleton-pulse 1.6s var(--ease) infinite;
}
.skeleton--title { block-size: 1.15rem; inline-size: 55%; }
.skeleton--line { block-size: 0.85rem; margin-block-end: var(--space-3); }
.skeleton--short { inline-size: 70%; }
.skeleton--block { block-size: 8rem; margin-block-start: var(--space-4); }
/* The profile sheet's skeleton is shaped like a profile — avatar disc, name, one
   line of metadata — rather than generic bars. A panel's skeleton is deliberately
   neutral because a panel could be anything; this one is only ever a profile, so it
   can be the right shape and the header does not jump when the real one lands. */
.skeleton--avatar {
  inline-size: 4.5rem; block-size: 4.5rem; border-radius: 50%; flex: none;
}
.skeleton-profile { padding-block: var(--space-4); }
.skeleton-profile__id {
  display: flex; align-items: center; gap: var(--space-4);
  margin-block-end: var(--space-5);
}
.skeleton-profile__who { flex: 1; min-inline-size: 0; display: grid; gap: var(--space-2); }
@keyframes skeleton-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.45; }
}
@media (prefers-reduced-motion: reduce) {
  .skeleton { animation: none; }
}
/* The loading frame is the panel's own box, so the real content lands in a
   container that is already the right size and place. */
.panel--loading .panel__body { min-block-size: 14rem; }

/* =============================================================================
   Thirteenth pass — the picker, and the builder's seams, 2026-08-12
   ========================================================================== */

/* ---- A picker grid, not a list of links ------------------------------------
   The owner on the form builder: *"think of this page as a very important page
   cause that makes users frustrated… maybe not a list and something even more
   easier and smart and quick to work with. also you can use icons."*

   Twelve text links are read line by line. A grid of tinted glyphs is scanned, and
   for a set where the *shape of the answer* is the whole difference — a line, a
   paragraph, a number, a date, a photo — the glyph carries more than the label
   does. This is the pattern Apple uses everywhere something is added from a fixed
   set: widgets, new documents, Shortcuts actions.

   `auto-fill` with a 8.5rem floor means one column on a narrow phone and three or
   four in a panel, without a breakpoint. */
.picker-grid {
  list-style: none; margin: 0 0 var(--space-5); padding: 0;
  display: grid; gap: var(--space-2);
  grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr));
}
.picker-tile {
  display: grid; gap: var(--space-1);
  block-size: 100%;
  padding: var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--fill-4);
  color: var(--text); text-decoration: none;
  transition: background var(--dur) var(--ease);
}
.picker-tile:hover { background: var(--fill-3); }
.picker-tile:focus-visible { outline: none; box-shadow: var(--ring); }
.picker-tile__icon { margin-block-end: var(--space-1); }
.picker-tile__label { font-weight: 600; line-height: 1.25; }
/* The hint stays, quietly. It is the difference between "Number" and "Number —
   height, weight, how many", and someone who has never built a form needs the
   second; someone who has is picking by glyph and never reads it. */
.picker-tile__hint {
  font-size: var(--step--1); color: var(--text-subtle); line-height: 1.35;
}
.panel__legend + .picker-grid { margin-block-start: var(--space-2); }

/* ---- The builder's three containers need a seam ----------------------------
   The owner: *"we might need a bit of spacing between sections, or some sort of
   divider, they're a bit mixed and not distinguishable."*

   Correct, and it appeared when the boxes became groups: the old `.builder__box`
   had a 1px border doing the separating, and removing it left three stacks of
   rows running into each other with only a heading between them. Space first —
   more of it than between the parts *inside* a container, so the grouping reads —
   and a hairline as the backstop for a container whose content happens to end
   flush against the next one's heading. */
.pane > .builder-step + .builder-step {
  margin-block-start: var(--space-6);
  padding-block-start: var(--space-6);
  border-block-start: 1px solid var(--hairline);
}
/* The per-step tints tried here on 2026-08-12 are **removed**. The owner:
   *"I think the different colors for number of each section isn't a good idea, the
   colors feels off and out of design."* They were right, and the rule written two
   passes earlier says why: a system colour names a *kind of thing*, and step 1, 2
   and 3 are not three kinds of thing — they are one thing, in order. Colouring them
   spent the palette's meaning on a distinction that position already makes. The
   badges stay in the accent, which is what "these are the steps" should look like. */

/* ---- Three things the tint broke, and one it exposed -----------------------
   Reported by the owner the same day the tinted page shipped. All four are the
   same mistake in different places: something drawn in `--surface-2` (`#f5f5f7`)
   used to sit on a `#fbfbfd` page and now sits on a `#f2f2f7` one, where it has
   almost no edge left. `--surface-2` is a *state on a surface*, never a surface —
   the rule written down when `.notice` hit this first, applied to the rest. */

/* 1. Inactive profile tabs. The owner: *"the second and third profile tabs (not
      active ones) too [are hard to see]"*. They were a `--surface-2` pill on the
      tinted page — a chip with no edge. White, with a hairline, so all three read
      as real controls and the checked one still clearly wins on colour. */
.tabs__label {
  background: var(--surface);
  border-color: var(--border);
  color: var(--text);
}
.tabs__label:hover { background: var(--accent-soft); border-color: transparent; color: var(--accent); }
.tabs__radio:checked + .tabs__heading .tabs__label { border-color: transparent; }

/* 2. The desktop menu. *"the text color is too close to the background or the
      hover needs to be more visible and closer to white."* The hover was
      `--surface-2` on a `--bg` bar: a 1% change nobody could see. White, and
      lifted, so the row you are pointing at is unmistakable. */
.nav-links a:hover {
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-sm);
}
.nav-sub a:hover { background: var(--surface-2); }

/* 3. The identity block. *"the profile hero section or identity section is so
      faded and dead, maybe we can give it a white background… the icons are a bit
      hard to see."*

      Two separate causes. It had no background at all, so it was the tint —
      correct before, washed out now — and its metadata row was `--text-muted`
      with icons inheriting it, which is the right weight for a caption under a
      card and too quiet for the first thing on the page. So: a real card, and the
      glyphs come up to the accent where they are links and to `--text-muted` only
      for the plain facts. */
.identity {
  background: var(--surface);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
}
.identity__meta { color: var(--text-muted); }
.identity__meta .icon { color: var(--accent); opacity: 0.9; }
.identity__meta a { color: var(--text); font-weight: 550; }

/* ---- Section titles carry hierarchy, not just a label ----------------------
   The owner: *"the title of each section beside the numbers should not be that
   faint grey, we need proper sizing and coloring to keep the content hierarchy
   mental model… that might or might not be compatible with apple styles and
   principles and if not, I accept them being the way [I] think they should be."*

   They are right and there is no conflict to accept. `.group-title` — small,
   uppercase, `--text-subtle` — is iOS's *group* caption: the quiet label above a
   list of rows, deliberately recessive because the rows are the content. A
   **section heading** is a different thing. macOS System Settings sets those at
   full text colour and a larger size, exactly because they are the level above.

   Using the group caption for both flattened the hierarchy: a numbered container
   holding three lists looked the same weight as one of the lists inside it. */
.builder-step__title,
.calendar-board__title,
.schedule-settings > summary {
  font-size: var(--step-1);
  font-weight: 650;
  color: var(--text);
  text-transform: none;
  letter-spacing: -0.01em;
}

/* ---- A panel has a ground, like a page does -------------------------------
   **This is the actual cause of "white on white", and it was never about the
   calendar.**

   Separation on the builder page does not come from the hairlines between its
   containers. It comes from white groups sitting on a tinted page — iOS's grouped
   background, which is why `--page-tint` exists and why `.sheet--settings` was
   given it back in August. A `<dialog class="panel">` is `--surface`, so the same
   groups arrive white-on-white and every seam inside them disappears.

   I answered the last report by adding hairlines *inside* the calendar screen. That
   treated the one screen the owner happened to be looking at and left every other
   panel with the same defect, which is exactly the failure this design system was
   written to stop — and it is why the next report was *"the ui of the calendar/
   schedule modal is a bit worsen"*: hairlines plus no ground is busier than plain
   white, not clearer.

   The ground goes on `.panel__body`, so head and foot stay `--surface` and keep
   reading as chrome over content. */
.panel__body { background: var(--page-tint); }

/* ---- Four containers on that ground ----------------------------------------
   *"we need four separate containers or sections."* Now that there is something to
   sit on, they can be containers rather than regions divided by rules — the same
   shape as the builder's, which the owner signed off: *"the sections in service
   builder are looking separate and that's cool."* */
.schedule-manager > * {
  background: var(--surface);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
}
.schedule-manager > * + * { margin-block-start: var(--space-4); }
.schedule-settings > summary { padding-inline: 0; }
.schedule-settings > summary:hover { background: none; color: var(--accent); }
.schedule-settings[open] > summary { box-shadow: none; margin-block-end: var(--space-3); }
.schedule-settings__body { padding: 0; }
.calendar-board { border: 0; box-shadow: none; }

/* Container 3 counted in the same voice as 1, 2 and 4.
   It was the odd one out: an eyebrow carrying the step chip and a `--step-2` date
   as its heading, beside three `--step-1` summaries. That is most of *"the coloring
   and separation and font sizes are not the same and proper"*. The title now matches
   its peers and the period is a second line under it — still the most prominent
   thing inside the container, and no longer competing with the container's own name. */
.calendar-board__period {
  margin: var(--space-1) 0 0;
  font-size: var(--step-1); font-weight: 650; color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* ---- The nested time-zone disclosure ---------------------------------------
   *"the timezone field inside that nested accordion isn't properly styled like
   other fields, also I have seen such mistreated style somewhere else."*

   Both instances are the same omission: a single `_field.html` dropped into a
   container that is not a `.form-list`, so it kept the bare label-above-input shape
   every other field left behind. Wrapping it is not enough on its own — a one-row
   form list with no card around it reads as a stray input — so it takes the card
   too, and the disclosure above it becomes a quiet row rather than a bordered box. */
.schedule-timezone { border-block-start: 0; padding-block-start: 0; }
.schedule-timezone > summary {
  min-block-size: 2.75rem; padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-sm); font-size: var(--step-0); font-weight: 600;
}
.schedule-timezone > summary:hover { background: var(--fill-4); }
.schedule-timezone__body { padding-block-start: var(--space-2); }

/* A settings row that carries an explanation as well as a label. The four request
   settings need it — "Opens and closes" is not self-evident — where the account
   sheet's rows ("Balance", "Notifications") never did. */
.settings-row__hint {
  display: block;
  font-size: var(--step--1); font-weight: 400; color: var(--text-subtle);
  line-height: 1.35;
}
.settings-row__label { display: grid; gap: 0.1rem; }
.settings-row > form { display: contents; }

/* ---- A panel's head stays put ----------------------------------------------
   The owner: *"the modals' head and close button aren't sticky, at least the close
   button should be sticky."* The close control is the one thing that must never be
   scrolled away from — a long form with the ✕ four screens up is a modal you feel
   trapped in, and on a phone there is no Escape key to fall back on.

   `.sheet` has had a sticky title since 2026-08-04; `.panel` never got it. Same
   treatment, and the same trap: sticky pins to the *padding* edge, so the head has
   to carry the panel's own background or content scrolls visibly underneath it. */
.panel__head {
  position: sticky; inset-block-start: 0; z-index: 2;
  background: var(--surface);
  box-shadow: inset 0 -1px 0 var(--hairline);
}
/* The foot is the other half of the pair: a Save button that scrolls off is the
   same problem pointing the other way. */
.panel__foot {
  position: sticky; inset-block-end: 0; z-index: 2;
  background: var(--surface);
  box-shadow: inset 0 1px 0 var(--hairline);
}

/* ---- Scrollbars inside our surfaces, restored ------------------------------
   These were never the cause of the sheet closing mid-drag — that was a scrollbar
   drag targeting the <dialog> exactly as a backdrop click does, and `dialog.js`
   now guards it by geometry. So styling them again is safe, and a thin scrollbar
   inside a rounded panel is one of the details that separates a native-feeling
   surface from a web page. Firefox takes the two-value form; WebKit needs the
   pseudo-elements. */
.panel, .sheet__body, .picker__list {
  scrollbar-width: thin;
  scrollbar-color: var(--fill-1) transparent;
}
.panel::-webkit-scrollbar, .sheet__body::-webkit-scrollbar { inline-size: 10px; block-size: 10px; }
.panel::-webkit-scrollbar-track, .sheet__body::-webkit-scrollbar-track { background: transparent; }
.panel::-webkit-scrollbar-thumb, .sheet__body::-webkit-scrollbar-thumb {
  background: var(--fill-1); border-radius: var(--radius-pill);
  border: 3px solid transparent; background-clip: content-box;
}
.panel::-webkit-scrollbar-thumb:hover, .sheet__body::-webkit-scrollbar-thumb:hover {
  background: var(--fill-2); background-clip: content-box;
}

/* The settings gear sits between the step's title and its switch, so the two
   controls that act on container three are together rather than at opposite ends
   of the row. */
.builder-step__gear { margin-inline-start: auto; }
.builder-step__gear + .builder-step__switch { margin-inline-start: 0; }

/* ---- The editing strip folds behind its gear -------------------------------
   Six controls per editable thing, on every section, block and service on the
   page, is a profile you cannot read while you are editing it. The owner asked
   for them behind a gear that stays where you put it, and for one gear to unfold
   all of them.

   One page-level checkbox does all of that with no script: `~ *` reaches every
   strip that follows it, so unfolding is a single state rather than a loop. */
/* **`fixed`, not `absolute`** — and that is the whole fix for a bug the owner hit
   immediately: *"when I click on the gear icon of the control bar, the page scrolls
   back up."*

   Activating a `<label>` moves focus to the control it names, and the browser scrolls
   a newly focused element into view. One checkbox for the page means that checkbox
   lives at the top of the profile, so every gear anywhere down the page threw the
   reader back to the start of it. A `position: fixed` element is always inside the
   visual viewport, so there is nothing to scroll to and the page stays put. It is
   still one state, still keyboard-reachable, still invisible. */
.edit-tools-toggle {
  position: fixed; inset-block-start: 0; inset-inline-start: 0;
  inline-size: 1px; block-size: 1px;
  opacity: 0; pointer-events: none;
}
.edit-tools__gear {
  display: grid; place-items: center; flex: none;
  inline-size: 2rem; block-size: 2rem;
  border-radius: 50%;
  color: var(--text-muted); cursor: pointer;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              transform var(--dur) var(--ease);
}
/* The grip stands beside the gear, outside the fold.

   The owner: *"the indicator and grip of the drag doesn't need to be in the folded
   control bar, it can be behind the gear icon in the first place."* It was *inside*
   `edit-tools__set`, so reordering meant unfolding five other controls to reach the
   handle. I read that as "remove it"; the correction was *"I just said get it out of
   control bar and put it behind the gear icon … so they sit together."*

   Two always-visible controls, then the five that fold. The grip only appears once
   `reorder.js` has actually made the list draggable — the same rule `.tree__grip` and
   `.gallery-edit__grip` follow, so a handle never advertises something that is not
   there. */
/* A row on its way out. Brief and cheap — opacity and nothing else, so it composites
   and cannot reflow the list under the pointer that is still deleting rows. */
.is-removing { opacity: 0.35; pointer-events: none; transition: opacity var(--dur) var(--ease); }
@media (prefers-reduced-motion: reduce) { .is-removing { transition: none; } }

.edit-tools__grip { display: none; }
.is-reorderable .edit-tools__grip { display: grid; cursor: grab; }
.is-reorderable .edit-tools__grip:active { cursor: grabbing; }
.edit-tools__gear:hover { background: var(--fill-3); color: var(--accent); }
.edit-tools-toggle:focus-visible ~ * .edit-tools__gear { outline: none; box-shadow: var(--ring); }

/* Folded is the default. `display: none` rather than a width transition: the strip
   is inside a layout that has to reflow around it, and an animated width leaves the
   buttons tabbable and screen-reader-visible while they are invisible. */
.edit-tools__set { display: none; align-items: center; gap: var(--space-1); }
.edit-tools-toggle:checked ~ * .edit-tools__set { display: inline-flex; }
/* The gear turns when its strip is open — the one piece of motion here, and it is
   the affordance that says the two states belong to the same control. */
.edit-tools-toggle:checked ~ * .edit-tools__gear {
  transform: rotate(45deg); color: var(--accent); background: var(--accent-soft);
}

/* On a pointer device, hovering the strip reveals it without committing to it —
   the owner: *"on desktop the control bar be opened on hover on gear icon and even
   stay open, but users can fold them back by clicking on the gear icon."* So hover
   is a peek and the click is the decision, and the click still wins because a
   checked checkbox keeps it open once the pointer leaves.

   `hover: hover` rather than a width breakpoint: a tablet is wide and has no
   hover, and revealing on a tap-through would fight the gear. */
@media (hover: hover) and (pointer: fine) {
  .edit-tools:hover .edit-tools__set,
  .edit-tools:focus-within .edit-tools__set { display: inline-flex; }
}
@media (prefers-reduced-motion: reduce) {
  .edit-tools__gear { transition: none; }
  .edit-tools-toggle:checked ~ * .edit-tools__gear { transform: none; }
}

/* ---- A top bar, not a back link --------------------------------------------
   Every owner screen had one control out of it and nothing else, which is a
   navigation bar with one button in it. Apple's settings screens put the way back
   on the leading edge and one or two destinations on the trailing edge, and that
   is what turns a dead end into a place you can move through.

   `space-between` on a bar whose second child is optional: with no action the back
   link simply sits at the leading edge, so a screen with nothing to offer there
   gets no empty box. */
.topbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3);
  margin-block-end: var(--space-2);
}
.topbar .page-head__back { margin-block-end: 0; }
.topbar__action {
  display: inline-flex; align-items: center; gap: var(--space-2);
  min-block-size: 2rem; padding-inline: var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--fill-4); color: var(--accent);
  font-size: var(--step--1); font-weight: 600; text-decoration: none;
  white-space: nowrap;
}
.topbar__action:hover { background: var(--accent-soft); }
.topbar__action:focus-visible { outline: none; box-shadow: var(--ring); }

/* ---- The appointment summary, as facts rather than a sentence --------------
   The last thing someone reads before committing to a time, on a phone. It was
   `start – end · 01:30` on one line, which makes the reader parse a sentence to
   check a fact. Label on the leading edge, value beside it, one fact per row —
   the shape every confirmation screen uses, for the same reason. */
.appointment-summary {
  display: grid; gap: var(--space-1);
  margin-block-start: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-sm);
  background: var(--fill-4);
  color: var(--text);
}
.appointment-summary__row {
  display: grid; grid-template-columns: minmax(4rem, auto) minmax(0, 1fr);
  gap: var(--space-1) var(--space-3);
  align-items: baseline;
}
/* The colon is drawn, not typed: it belongs to the label's presentation, and in a
   right-to-left line a typed one lands on the wrong side of the word. */
.appointment-summary__label { font-weight: 650; }
.appointment-summary__label::after { content: ":"; }
.appointment-summary__value { font-variant-numeric: tabular-nums; }

/* ---- Every "choose file" control, in one place -----------------------------
   The owner: *"let's also redesign all choose file fields used accross the
   platform with proper style and label like apple principle and suitable
   positioning."*

   There are four of them — the profile photo, gallery pictures, a payment receipt,
   and any `file` question an owner adds to their form — and they were a raw
   `<input type="file">` wearing `.input`, which is the *text field* style. So they
   looked like something you type into, and the browser's own grey button sat inside
   that box in whatever shape the platform felt like.

   Restyled with a `<label>` wrapping a hidden input, **not** with
   `::file-selector-button`.

   The pseudo-element was the first answer here and it was the wrong one, for a
   reason that only shows up in a second language: it styles the native button but
   cannot change its *words*. Those come from the browser's locale, as does "no file
   chosen", and nothing on the page can reach either. So the control was restyled and
   still read in English on a Persian site. The owner found it one round later:
   *"the choose file element isn't translated."*

   The label costs what the note here used to say it costs — the filename readout has
   to be rebuilt, which `file-field.js` does — and buys the only thing that mattered.
   Keyboard focus is not among the costs: focus stays on the real input, and the ring
   is drawn on the wrapper through `:focus-within`.

   Apple's shape, unchanged: the action reads as a button, the state reads as text
   beside it, and the two never merge into one grey slab. */
.file-field {
  display: flex; align-items: center; gap: var(--space-3);
  inline-size: 100%;
  padding: var(--space-2);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--fill-4);
  color: var(--text-muted);
  font-size: var(--step--1);
  cursor: pointer;
}
.file-field:hover { border-color: var(--accent); background: var(--accent-soft); }
.file-field:focus-within { outline: none; box-shadow: var(--ring); border-color: var(--accent); }
/* Hidden, never `display: none` — the input must stay focusable and must stay a
   submitted control of its form. */
.file-field input[type="file"] {
  position: absolute; inline-size: 1px; block-size: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
.file-field__btn {
  flex: none;
  display: inline-flex; align-items: center;
  min-block-size: 2.25rem; padding-inline: var(--space-4);
  border-radius: var(--radius-pill);
  background: var(--surface); color: var(--accent);
  box-shadow: var(--shadow-sm);
  font-weight: 600;
}
.file-field:hover .file-field__btn { background: var(--accent); color: var(--on-accent); }
.file-field__name { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.file-field--chosen .file-field__name { color: var(--text); font-weight: 550; }
/* A dashed box is right for "drop something here" and wrong once something is in
   it, so the widget that knows it has a result says so. */
.image-upload:has([data-role="result"]:not([hidden])) .file-field {
  border-style: solid; border-color: var(--border);
}

/* ---- Sharing one menu ------------------------------------------------------
   A service's menu has its own page, for a printed code on a table to point at
   (`docs/product/shareable-menu.md`). This is the panel an owner reads it from.

   The code is inline SVG on a white plate, always — **not** the surface colour and
   not theme-aware. A QR code is black on white or it is a scanning problem: dark mode
   would invert it, and an inverted code is unreadable to a good half of the scanners
   in the world. It is the one thing in this stylesheet that ignores the theme, and
   that is the correct answer rather than an oversight. */
.share-qr {
  display: grid; place-items: center;
  padding: var(--space-4);
  background: #ffffff;
  border-radius: var(--radius);
  border: 1px solid var(--border);
}
.share-qr svg { inline-size: min(14rem, 60vw); block-size: auto; }
.share-link {
  display: grid; gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
}
/* The address stays left-to-right inside a right-to-left page — a URL reads one way
   in every language, and mirroring it puts the domain after the path. */
.share-link .input { direction: ltr; text-align: start; font-size: var(--step--1); }
@media (min-width: 30rem) {
  .share-link { grid-template-columns: 1fr auto; align-items: end; }
  .share-link .group-title { grid-column: 1 / -1; }
}

/* ---- The standalone menu page ----------------------------------------------
   One service's menu, on its own, for a printed code to point at
   (`docs/product/shareable-menu.md`). Narrower than a profile and quieter: the
   reader has just scanned a card and wants a price, so the page is a header and a
   list and nothing else competing for the top of the screen. */
.menu-page { max-inline-size: 34rem; margin-inline: auto; padding-block: var(--space-5); }
.menu-page__head {
  display: flex; align-items: center; gap: var(--space-4);
  margin-block-end: var(--space-5);
}
.menu-page__who { min-inline-size: 0; }
.menu-page__business {
  font-size: var(--step--1); color: var(--text-subtle); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.05em;
}
.menu-page__title { font-size: var(--step-2); margin-block: var(--space-1) 0; }
.menu-page__section { font-size: var(--step--1); color: var(--text-muted); }
.menu-page__note {
  color: var(--text-muted); margin-block-end: var(--space-5);
}
/* On its own page the menu is the content, not a panel inside an accordion, so it
   takes the card the profile's version inherits from the offering around it. */
.menu--page {
  margin-block: 0;
  background: var(--surface);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
}
.menu--page .menu__row:first-child { border-block-start: 0; padding-block-start: 0; }
.menu-page__foot { margin-block-start: var(--space-6); text-align: center; }
