:root {
  /* SPACING — em, so padding/gaps scale with the component's own text size.
     This is the proportional behaviour em is genuinely good at. */
  --space-xs: 0.25em;
  --space-sm: 0.5em;
  --space-md: 0.75em;
  --space-lg: 1em;
  --space-xl: 1.5em;
  --radius: 0.5em;
  /* [TARGETED: 2026-07-17] iOS 26 "Liquid Glass" tokens.
     Apple publishes NO exact CSS px/opacity numbers (verified — the HIG states
     the design language, not the pixels). These are the converging community
     values, each labelled. All em-based so they scale with the type. */
  --glass-blur: 16px;        /* 8-16px is the converging range for the frosted look */
  --glass-saturate: 160%;    /* keeps colour vivid behind the blur, not grey mud */
  --glass-radius: 1.25em;    /* iOS 26 uses larger, continuous corners */
  /* Concentric child radius = outer minus padding, Apple's own subtraction rule. */
  --glass-pad: var(--space-xs);
  --glass-radius-item: calc(var(--glass-radius) - var(--glass-pad));
  /* A spring-approximating ease (SwiftUI default response 0.55 / damping 0.825).
     A cubic-bezier cannot BE a spring, but this overshoots then settles like one. */
  --glass-ease: cubic-bezier(0.34, 1.56, 0.64, 1);

  /* TYPE SCALE — rem, deliberately NOT em.
     em multiplies down the ancestor chain, so a 0.75em badge inside a 0.875em
     table inside a 0.875em body rendered at 9.2px (0.75 x 0.875 x 0.875 x 16)
     — measured, not theoretical. rem always resolves against the root, so a
     size means the same thing at any nesting depth, and it still honours a
     user's browser font-size preference (which px would ignore).
     Floor is --text-xs (12px): below that, text stops being readable. */
  --text-xs: 0.75rem;    /* 12px — badges, meta, legends */
  --text-sm: 0.8125rem;  /* 13px — table body, secondary */
  --text-base: 0.875rem; /* 14px — body default */
  --text-md: 1rem;       /* 16px — h3, emphasis */
  --text-lg: 1.25rem;    /* 20px — h2 */
  --text-xl: 1.5rem;     /* 24px — h1 */

  /* Amber Slate theme — light */
  --bg-page: oklch(0.98 0.005 95);
  --bg-panel: oklch(1 0 0);
  --text-primary: oklch(0.22 0.02 260);
  --text-secondary: oklch(0.46 0.02 260);

  --border-light: oklch(0.93 0.008 260);
  --border-default: oklch(0.88 0.01 260);
  --border-medium: oklch(0.78 0.015 260);
  --border-dark: oklch(0.60 0.02 260);

  --muted: oklch(0.95 0.01 260);
  --accent: oklch(0.90 0.05 75);

  --natural-slate: oklch(0.50 0.02 260);
  --natural-stone: oklch(0.50 0.015 70);
  --primary: oklch(0.72 0.16 65);
  --primary-fg: oklch(0.15 0.02 60);
  --accent-fg: oklch(0.15 0.02 60);
  --natural-green: #22c55e;
  --natural-red: #ef4444;
  --natural-amber: oklch(0.72 0.16 65);
  /* [TARGETED: 2026-07-19] Readable near-white text/border ON a SATURATED colour
     (a red/green/primary button, a coloured overlay). Theme-INDEPENDENT: a red
     button is red in both themes, so its contrast text is the same. Replaces
     scattered literal #fff so "var must be used" holds everywhere.
     --on-strong-dim = the same at reduced alpha, for subtle borders/insets. */
  --on-strong: oklch(0.99 0 0);
  --on-strong-dim: oklch(0.99 0 0 / 0.35);
  /* Dark scrim placed OVER media (a delete button on an image, a caption bar) —
     theme-independent because it sits on an arbitrary photo, not on a themed
     surface. Replaces literal rgba(0,0,0,0.55). */
  --overlay-scrim: oklch(0 0 0 / 0.55);

  /* Status badge surfaces. These exist because pages were hardcoding light
     pastels (#f0fdf4, #fde68a, #fecaca) paired with a token text colour —
     which measured 2.18:1 in LIGHT mode (WCAG AA needs 4.5) and kept a bright
     pastel background in dark mode while the panel behind it went dark.
     bg+fg are paired per status so both schemes stay readable. */
  --status-ok-bg: oklch(0.95 0.05 150);
  --status-ok-fg: oklch(0.40 0.13 150);
  --status-warn-bg: oklch(0.95 0.06 85);
  --status-warn-fg: oklch(0.45 0.13 70);
  --status-bad-bg: oklch(0.95 0.04 25);
  --status-bad-fg: oklch(0.45 0.18 25);

  /* Toggle switch. --natural-green was too bright to carry a white knob: it
     measured 2.28:1 against white, where WCAG 1.4.11 needs 3:1 for a UI
     component. These are darker so the knob reads at 4.54:1 while the track
     still stands off the panel behind it. */
  --switch-on: oklch(0.55 0.15 150);
  --switch-knob: oklch(1 0 0);

  /* [TARGETED: 2026-07-19] Elevation shadow tokens. Components were hardcoding
     rgba(0,0,0,0.18) etc. inline — untokenised, and identical in dark mode where
     a black shadow on a dark panel is invisible. Three levels, tuned in LIGHT
     here and overridden DARKER + stronger in the dark block so elevation still
     reads. Use these instead of literal box-shadow rgba(). */
  --shadow-sm: 0 0.125em 0.5em rgba(0, 0, 0, 0.10);
  --shadow-md: 0 0.25em 0.75em rgba(0, 0, 0, 0.16);
  --shadow-lg: 0 0.375em 1.25em rgba(0, 0, 0, 0.22);

  /* [TARGETED: 2026-07-19] Motion tokens (Material 3 easing/duration scale) so
     component transitions reference one source instead of inline literals. 150ms
     + standard easing is the "polished, not gimmicky" zone. */
  --duration-fast: 0.15s;
  --duration-med: 0.2s;
  --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg-page: oklch(0.20 0.02 260);
    --bg-panel: oklch(0.24 0.02 260);
    --text-primary: oklch(0.95 0.01 95);
    --text-secondary: oklch(0.75 0.02 260);

    --border-light: oklch(0.30 0.02 260);
    --border-default: oklch(0.32 0.02 260);
    --border-medium: oklch(0.42 0.02 260);
    --border-dark: oklch(0.55 0.02 260);

    --muted: oklch(0.28 0.02 260);
    --accent: oklch(0.35 0.06 75);

    --status-ok-bg: oklch(0.30 0.06 150);
    --status-ok-fg: oklch(0.85 0.15 150);
    --status-warn-bg: oklch(0.32 0.07 85);
    --status-warn-fg: oklch(0.85 0.14 85);
    --status-bad-bg: oklch(0.30 0.08 25);
    --status-bad-fg: oklch(0.82 0.14 25);

    /* Lighter on dark: the track must read against the dark panel (5.44:1)
       while still carrying the white knob (3.02:1). */
    --switch-on: oklch(0.65 0.16 150);
    --switch-knob: oklch(1 0 0);

    --primary-fg: oklch(0.15 0.02 60);
    /* Light text on the dark accent. Reusing --primary-fg here measured
       1.73:1 — near-invisible. --accent-fg exists because no single token can
       sit on BOTH --primary (bright amber, needs dark text) and --accent
       (dark in this scheme, needs light text). */
    --accent-fg: oklch(0.95 0.01 95);

    /* Deeper shadows on dark: a faint black shadow vanishes against a dark
       panel, so dark mode needs a heavier alpha to keep elevation legible. */
    --shadow-sm: 0 0.125em 0.5em rgba(0, 0, 0, 0.35);
    --shadow-md: 0 0.25em 0.75em rgba(0, 0, 0, 0.45);
    --shadow-lg: 0 0.375em 1.25em rgba(0, 0, 0, 0.55);
  }
}

* {
  box-sizing: border-box;
}

/* [TARGETED: 2026-07-19] GLOBAL OVERFLOW SAFETY NET — nothing overflows the
   screen horizontally, on any page (static, dynamic, admin) or any device.
   Researched against the standard fixes (LogRocket, CSS-Tricks, MDN):

   1. The last-line guard on the document root: a horizontal scrollbar can never
      appear. max-width:100% + overflow-x:clip contains any stray wide child.
      overflow-x:clip (not hidden) does NOT create a scroll container, so it will
      not break position:sticky headers/footers the way `hidden` can. */
html {
  max-width: 100%;
  overflow-x: clip;
}
body {
  margin: 0;
  max-width: 100%;
  overflow-x: clip;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  /* rem, not em: as the compounding ROOT this is the value every nested em
     multiplied against. */
  font-size: var(--text-base);
  line-height: 1.4;
  background: var(--bg-page);
  color: var(--text-primary);
}

/* 2. Replaced media never exceeds its container — the #1 cause of overflow.
      A user-uploaded photo or a wide record image now scales down instead of
      pushing the page. height:auto keeps aspect ratio. */
img, svg, video, canvas, iframe, embed, object {
  max-width: 100%;
}
img, video, canvas { height: auto; }

/* 3. Long unbroken strings (a pasted URL, a long token, a no-space language)
      wrap instead of forcing a wide row. Applied broadly but overridable. */
body {
  overflow-wrap: break-word;
  word-break: normal;
}

/* 4. The classic flexbox/grid overflow gotcha: a flex/grid CHILD defaults to
      min-width:auto, so it refuses to shrink below its content and pushes the
      row wider than the screen. min-width:0 lets it shrink. Scoped to the app's
      own layout containers so it cannot surprise a deliberately-min-sized item. */
:where(.public-shell, .msg-page, .msvc-page, .addlst-page, .account-shell, .public-main, .addlst-main)
  :where(main, section, article, .card, [class$="__main"], [class*="__body"]) {
  min-width: 0;
}

h1 { font-size: var(--text-xl); margin: 0 0 var(--space-sm) 0; }
h2 { font-size: var(--text-lg); margin: 0 0 var(--space-sm) 0; }
h3 { font-size: var(--text-md); margin: 0 0 var(--space-xs) 0; }
p { margin: 0 0 var(--space-sm) 0; }

.btn, button, a, .card, .nav-item, input, select, textarea, .icon {
  transition: all 0.2s ease-out;
}

*:focus {
  outline: none;
}

/* [TARGETED: 2026-07-20] [hidden] MUST win over any class-based display rule,
   sitewide, always. A bare attribute selector and a single class selector are
   the SAME specificity (0,1,0) — whichever is declared LATER in the cascade
   wins the tie, which made `.modq__empty { display: flex }` (declared after
   this point) silently defeat `element.hidden = true` set by JS. Confirmed
   live: moderation.html showed the skeleton, the empty state, AND the error
   box simultaneously — none of their `hidden` attributes were doing anything,
   because each had its own `display: flex` rule with equal-but-later
   specificity. Individual components (.hero-gallery__nav[hidden],
   .public-menu__panel[hidden], .pwa-install[hidden]) each got a one-off
   `[hidden] { display: none }` override as this was found page by page — this
   is the one rule that makes every FUTURE component safe by default, so a new
   `display: flex`/`grid`/whatever class never has to remember to add its own
   override. `!important` is deliberate: this is the ONE place in base.css a
   component must never be able to out-rank, by construction. */
[hidden] { display: none !important; }

.icon {
  width: 1.25em;
  height: 1.25em;
  stroke: currentColor;
  fill: none;
  stroke-width: 1;
}

.icon:hover {
  transform: scale(1.15);
  opacity: 0.85;
}

/* Solid (filled) icons.
   .icon sets `fill: none; stroke: currentColor` for the outline set. A filled
   path under those rules renders as NOTHING — it has no stroke to draw and its
   fill is switched off. This variant inverts both, so a fill-based mark (the
   metalworking anvil) can sit alongside the outline icons and still inherit
   colour from currentColor like everything else. */
.icon--solid {
  fill: currentColor;
  stroke: none;
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  background: var(--muted);
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
  color: var(--text-primary);
  padding: 0.5em 1em;
  font-family: inherit;   /* buttons don't inherit typography either */
  font-size: var(--text-base);
  cursor: pointer;
  /* [TARGETED: 2026-07-20] .btn is applied to BOTH <button> and <a> — a real
     <button> never has an underline, so an <a class="btn"> must not either or
     the two look like different components. Found via the header rebuild:
     public-account.ejs had worked around this with an inline
     style="text-decoration:none" on ONE of its 11 <a class="btn"> instances
     across the app; the other 10 (public-home, public-browse, public-record,
     etc.) all had the same gap, unfixed. One rule here instead of one inline
     style per instance. */
  text-decoration: none;
}

.btn:hover {
  background: var(--accent);
  border-color: var(--border-medium);
  transform: translateY(-2px);
  box-shadow: 0 0.25em 0.75em rgba(0, 0, 0, 0.08);
}

.btn:active {
  transform: scale(0.98) translateY(0);
  box-shadow: 0 0.125em 0.25em rgba(0, 0, 0, 0.1);
}

.btn--primary {
  background: var(--primary);
  border-color: var(--primary);
  color: var(--primary-fg);
}

.btn--primary:hover {
  filter: brightness(1.08);
}

/* ===========================================================================
   FORM CONTROLS — element-selector BASELINE, class-based variants
   ===========================================================================
   Every control is styled by ELEMENT, not by class. Styling only via `.field`
   meant an input without that class rendered as a raw browser default — 17 of
   them shipped that way, because the design required every author to remember
   a class on every control. A baseline cannot be forgotten.

   Specificity is deliberate and load-bearing:
     input, select, textarea  -> (0,0,1)  element only
     .field, .token-row__input-> (0,1,0)  class ALWAYS wins
   So this is a floor, never a ceiling: it can never override a component's own
   styling. It is bare (no [type=...] attribute) precisely for this reason —
   `input[type="text"]` would score (0,1,1) and beat every existing class.

   Controls with a custom-built replacement (checkbox/radio/toggle/file) are
   excluded: those are visually hidden and drawn by a sibling element, so
   styling them here would resurrect the native box we deliberately hide.
   =========================================================================== */

/* Native widgets (number spinners, date pickers, scrollbars) are drawn by the
   browser and ignore our colours. color-scheme tells it which palette to use —
   without this, a date picker stays light-on-light in dark mode. */
:root { color-scheme: light dark; }
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }

input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="hidden"]),
select,
textarea {
  /* Font properties do NOT inherit into form controls the way they do into
     ordinary elements — without this every input silently falls back to the
     browser's default font and size, ignoring the design system entirely. */
  font-family: inherit;
  font-size: var(--text-base);
  line-height: 1.4;
  color: var(--text-primary);
  background: var(--bg-panel);
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
  padding: 0.5em 0.75em;
  width: 100%;
  transition: border-color 0.15s ease-out, box-shadow 0.15s ease-out;
}

/* accent-color tints the controls we do NOT replace (range, progress) plus the
   caret, so even native widgets follow the DB-driven theme. */
input, select, textarea { accent-color: var(--primary); }

input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):hover,
select:hover,
textarea:hover {
  border-color: var(--border-medium);
}

/* :focus-visible, not :focus — a mouse click should not draw a focus ring, but
   keyboard navigation must. base.css sets `*:focus { outline: none }`, so
   without this rule keyboard users would have NO visible focus indicator. */
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):focus-visible,
select:focus-visible,
textarea:focus-visible {
  border-color: var(--primary);
  box-shadow: 0 0 0 0.1875em color-mix(in oklch, var(--primary) 20%, transparent);
  /* Transparent outline stays visible in Windows High Contrast mode, where
     box-shadow is discarded. */
  outline: 0.1875em solid transparent;
}

input[readonly], input:disabled,
select:disabled,
textarea[readonly], textarea:disabled {
  background: var(--muted);
  color: var(--text-secondary);
  cursor: not-allowed;
}

/* Only tint the border once the user has actually typed something invalid —
   :invalid alone would paint every required field red before it's touched. */
input:not(:placeholder-shown):invalid,
textarea:not(:placeholder-shown):invalid {
  border-color: var(--natural-red);
}

textarea { min-height: 5em; resize: vertical; }

::placeholder { color: var(--text-secondary); opacity: 0.7; }

/* Native select arrow varies per browser/OS, so it is replaced with our own
   SVG-shaped caret drawn in CSS (no emoji, no image request). */
select {
  appearance: none;
  -webkit-appearance: none;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--text-secondary) 50%),
    linear-gradient(135deg, var(--text-secondary) 50%, transparent 50%);
  background-position:
    calc(100% - 1.05em) calc(50% + 0.1em),
    calc(100% - 0.7em) calc(50% + 0.1em);
  background-size: 0.35em 0.35em, 0.35em 0.35em;
  background-repeat: no-repeat;
  padding-inline-end: 2em;   /* logical — mirrors under RTL */
  cursor: pointer;
}

/* .field remains for explicit opt-in and back-compat. It now matches the
   baseline, so pages using it and pages that forgot it look identical. */
.field {
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
  padding: 0.5em 0.75em;
  font-size: var(--text-base);
  font-family: inherit;
  width: 100%;
  background: var(--bg-panel);
  color: var(--text-primary);
}

.field:hover {
  border-color: var(--border-medium);
}

.field:focus-visible {
  border-color: var(--primary);
  box-shadow: 0 0 0 0.1875em color-mix(in oklch, var(--primary) 20%, transparent);
  outline: 0.1875em solid transparent;
}

.field[readonly], .field:disabled {
  background: var(--muted);
  color: var(--text-secondary);
  cursor: not-allowed;
}

/* ===========================================================================
   APP HEADER + NAVIGATION (shared by every admin page)
   ===========================================================================
   Navigation links are LINKS, not buttons. Buttons and links are not
   interchangeable: a button performs an action (logout, save), a link changes
   the URL. Styling nav links as heavy .btn chips — as home.html did — makes
   every destination compete visually with the real actions beside it and
   causes hesitation and misclicks. Buttons stay reserved for actions.

   Measured before this change: 7 .btn chips in one row needed ~1034px and
   overflowed every viewport under 1024px (by 644px on a 390px phone). There
   was no mobile menu on any page.
   =========================================================================== */

.app-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-md) var(--space-lg);
  background: var(--bg-panel);
  border-bottom: 1px solid var(--border-default);
  position: relative;
}

.app-header__brand {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex-shrink: 0;
}

/* Sitewide brand mark (the anvil, ICON_MODULE_SOLID in icons.js) — a fixed
   logo, not a clickable control, so it opts out of .icon:hover's scale-up. */
.app-header__brand > .icon { color: var(--primary); width: 1.6em; height: 1.6em; }
.app-header__brand > .icon:hover { transform: none; opacity: 1; }
.app-header__brand > h3,
.app-header__brand > strong { margin: 0; white-space: nowrap; }

/* Nav + user cluster share one flex row on desktop. */
.app-header__end {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  min-width: 0;
}

.app-nav {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

.app-nav__link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: 0.4em 0.7em;
  border-radius: 0.375em;
  border: 1px solid transparent;   /* reserved so hover doesn't shift layout */
  color: var(--text-secondary);
  text-decoration: none;
  font-size: var(--text-sm);
  white-space: nowrap;
}

.app-nav__link:hover {
  background: var(--muted);
  color: var(--text-primary);
  opacity: 1;                       /* override the global a:hover fade */
}

/* Current page: filled, and aria-current is the accessible source of truth. */
.app-nav__link.is-active,
.app-nav__link[aria-current="page"] {
  background: var(--accent);
  border-color: var(--border-medium);
  color: var(--accent-fg);
  font-weight: 500;
}

.app-nav__link > .icon { width: 1em; height: 1em; }
.app-nav__link:hover > .icon { transform: none; }  /* no jitter inside a link */

.app-header__profile-tag {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  background: var(--accent);
  border: 1px solid var(--border-default);
  color: var(--accent-fg);
  border-radius: 0.375em;
  padding: 0.25em 0.625em;
  font-size: var(--text-xs);
  white-space: nowrap;
}

/* ===========================================================================
   [TARGETED: 2026-07-20] HEADER REBUILD — the admin USER MENU.
   Replaces the 3 hand-written "Sign out" buttons (one of which, on Statistics,
   had no click handler at all) with one component (app-header-user.js) mounted
   on every admin page. Researched: role="menu"/"menuitem" is correct here
   specifically because this IS an application menu of actions (settings,
   sign out) — not site navigation, where the W3C APG says role="menu" is the
   wrong tool (app-nav.js's <nav> links stay plain links, deliberately).
   DESKTOP and MOBILE are two different presentations of the SAME markup, not
   two components: desktop shows name+profile inline next to a small avatar;
   mobile collapses to the avatar alone (the name is one tap away inside the
   panel) so it never competes with the hamburger for space in a cramped row.
   =========================================================================== */
.user-menu { position: relative; flex-shrink: 0; }

.user-menu__trigger {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  padding: 0.25em 0.5em 0.25em 0.25em;
  background: transparent;
  border: 1px solid var(--border-default);
  border-radius: 999px;
  cursor: pointer;
  color: var(--text-primary);
}

.user-menu__trigger:hover { background: var(--muted); border-color: var(--border-medium); }
.user-menu__trigger:focus-visible {
  border-color: var(--primary);
  box-shadow: 0 0 0 0.1875em color-mix(in oklch, var(--primary) 25%, transparent);
  outline: 0.1875em solid transparent;
}

.user-menu__avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75em;
  height: 1.75em;
  border-radius: 50%;
  background: var(--accent);
  color: var(--accent-fg);
  font-size: var(--text-xs);
  font-weight: 700;
  flex-shrink: 0;
  overflow: hidden;
}

.user-menu__avatar--lg { width: 2.5em; height: 2.5em; font-size: var(--text-md); }

/* [TARGETED: 2026-07-20] Public member variant — a real uploaded photo, not
   just an initial. Same .user-menu__avatar circle (public-user-menu.js reuses
   every admin user-menu class, per the research: an avatar-triggered account
   menu is the "most personal, farthest-right" pattern regardless of which
   session type is signed in) — the <img> just fills the SAME circle instead
   of showing a letter, so admin (initial) and member (photo) read as one
   consistent component, not two different ones. */
.user-menu__avatar > img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.user-menu__tag { display: flex; flex-direction: column; align-items: flex-start; line-height: 1.2; min-width: 0; }
.user-menu__name { font-size: var(--text-sm); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 10em; }
.user-menu__profile, .user-menu__username { font-size: var(--text-xs); color: var(--text-secondary); }

.user-menu__chevron { width: 0.9em; height: 0.9em; color: var(--text-secondary); transition: transform 0.15s ease-out; }
.user-menu__trigger[aria-expanded="true"] .user-menu__chevron { transform: rotate(180deg); }

/* [TARGETED: 2026-07-20] position:absolute here is only the CLOSED-state
   default — verified live that it does NOT work as the open-state layout.
   A native [popover] renders in the top layer, which breaks the normal
   containing-block chain: absolute positioning resolved against the
   VIEWPORT, not .user-menu, and the panel measured 900+px offscreen. CSS
   Anchor Positioning is the "correct" native fix but sits at ~91% browser
   support; app-header-user.js instead sets position:fixed + explicit
   top/right via getBoundingClientRect() every time the panel opens, for both
   the popover and manual-fallback paths — this rule only matters before that
   JS runs (i.e., never visibly, since [popover]/[hidden] keeps it invisible
   until then). */
.user-menu__panel {
  position: absolute;
  inset-inline-end: 0;
  top: calc(100% + 0.5em);
  margin: 0;
  min-width: 14em;
  padding: 0.5em;
  background: var(--bg-panel);
  border: 1px solid var(--border-default);
  border-radius: 0.75em;
  box-shadow: var(--shadow-lg, 0 0.75em 2em rgba(0, 0, 0, 0.18));
  z-index: 30;
}

/* The native popover renders in the top layer with UA defaults (border,
   inset:0) that fight the styles above — :popover-open scopes the reset to
   exactly the state that needs it, so the [hidden] fallback path (older
   browsers) is untouched. */
.user-menu__panel:popover-open { display: block; }

.user-menu__panel-head { display: flex; align-items: center; gap: var(--space-sm); padding: var(--space-xs) var(--space-sm) var(--space-sm); }
.user-menu__divider { height: 1px; background: var(--border-light, var(--border-default)); margin: 0.25em 0; }

.user-menu__item {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  width: 100%;
  padding: 0.6em 0.75em;
  background: transparent;
  border: none;
  border-radius: 0.5em;
  color: var(--natural-red);
  font-size: var(--text-sm);
  text-align: start;
  cursor: pointer;
}

.user-menu__item:hover { background: color-mix(in oklch, var(--natural-red) 8%, transparent); }
.user-menu__item:focus-visible { outline: 0.125em solid var(--primary); outline-offset: -0.125em; }
.user-menu__item > .icon { width: 1.1em; height: 1.1em; flex-shrink: 0; }

@media (max-width: 60em) {
  /* The name/profile text is one tap away inside the panel — showing it again
     in the trigger would fight the hamburger for the same cramped row. */
  .user-menu__trigger { padding: 0.25em; gap: 0; border-color: transparent; }
  .user-menu__trigger .user-menu__tag { display: none; }
  .user-menu__trigger .user-menu__chevron { display: none; }
  .user-menu__avatar { width: 2em; height: 2em; }
}

/* Theme toggle — a real <button>: it performs an action, not navigation.
   Cycles System -> Light -> Dark. The icon shows the CURRENT state. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.4em;
  background: transparent;
  border: 1px solid var(--border-default);
  border-radius: 0.375em;
  color: var(--text-secondary);
  cursor: pointer;
  line-height: 0;
  flex-shrink: 0;
}

.theme-toggle:hover {
  background: var(--muted);
  border-color: var(--border-medium);
  color: var(--text-primary);
}

.theme-toggle:focus-visible {
  border-color: var(--primary);
  box-shadow: 0 0 0 0.1875em color-mix(in oklch, var(--primary) 25%, transparent);
  outline: 0.1875em solid transparent;
}

.theme-toggle > .icon { width: 1.1em; height: 1.1em; }
/* The button already scales on hover; scaling the icon too reads as jitter. */
.theme-toggle:hover > .icon { transform: none; }

/* Pages with no header (login, register, profile, error) still need the
   toggle. This corner slot mirrors .lang-switcher's placement so the two sit
   together instead of one floating alone. inset-inline-end is logical, so it
   moves to the left under RTL — same as the language switcher. */
/* [TARGETED: 2026-07-16] Children of #pageControls draw NO chrome and claim NO
   position — the cluster already drew the border, background and radius, and
   clips them with overflow:hidden. A child that positioned itself would escape
   the cluster; that was the original overlap bug. :where() keeps these at
   specificity (0,0,0) so a page can still override without !important. */
:where(.lang-switcher, .lang-switcher--inline) {
  position: static;      /* never absolute inside the cluster */
  display: flex;
  align-items: center;
  background: none;
  border: none;
  border-radius: 0;
}

.theme-toggle--corner {
  position: absolute;
  top: var(--space-lg);
  inset-inline-end: var(--space-lg);
  z-index: 10;
  background: var(--bg-panel);
}

/* [TARGETED: 2026-07-16] The old "shift the toggle 3.5em clear of the switcher"
   rule lived here. It is deliberately GONE: #pageControls now holds both
   controls in one flex row, so nudging them apart re-creates the drift. The
   cluster owns position; children own none. */

/* Hamburger: hidden on desktop, revealed at the breakpoint. A real <button>
   because it performs an action (toggling) rather than navigating. */
.app-nav__toggle {
  display: none;
  align-items: center;
  justify-content: center;
  padding: 0.4em;
  background: transparent;
  border: 1px solid var(--border-default);
  border-radius: 0.375em;
  color: var(--text-primary);
  cursor: pointer;
  line-height: 0;
}

.app-nav__toggle:hover { background: var(--muted); border-color: var(--border-medium); }
.app-nav__toggle > .icon-close { display: none; }
.app-nav__toggle[aria-expanded="true"] > .icon-close { display: block; }
.app-nav__toggle[aria-expanded="true"] > .icon-open { display: none; }

/* MOBILE: the row cannot hold 7 destinations, so it collapses to a panel.
   60em (960px) is where the measured 1034px requirement stops fitting. */
@media (max-width: 60em) {
  .app-nav__toggle { display: inline-flex; }

  .app-nav {
    display: none;
    position: absolute;
    top: 100%;
    inset-inline: 0;               /* logical: mirrors correctly under RTL */
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: var(--space-sm);
    background: var(--bg-panel);
    border-bottom: 1px solid var(--border-default);
    box-shadow: 0 0.5em 1em rgba(0, 0, 0, 0.08);
    z-index: 20;
  }

  .app-nav.is-open { display: flex; }

  .app-nav__link {
    padding: var(--space-md);
    border-radius: 0.375em;
    font-size: var(--text-base);   /* touch target, not a dense desktop row */
  }

  /* The tag is decorative next to the username; the header stays uncluttered. */
  .app-header__profile-tag { display: none; }
}

/* ===========================================================================
   SUMMARY CARD GRID + MODAL — shared by the admin list pages
   ===========================================================================
   auto-fit + minmax reflows the columns by ITSELF: 1 column on a phone, 2 on a
   tablet, 3-4 on a desktop, with no media queries and no breakpoints to keep in
   sync. The old layout was a fixed `20em 1fr` pane splitter, which is the
   pattern that breaks on narrow screens.
   =========================================================================== */

/* The app shell. Every admin page has this exact structure, so it belongs here
   — .app-main padding was copy-pasted into 6 pageStyles, and the pages that
   forgot it (modules, fields, public-site) rendered flush against the viewport
   edge with no breathing room at all. */
.app-shell {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.app-shell > main.app-main {
  flex: 1;
  /* Vertical breathing room > horizontal: content is read down the page, and on
     a phone a wide side gutter wastes scarce width. */
  padding: var(--space-xl) var(--space-lg);
}

/* Below ~40em the extra-large vertical padding costs more than it gives. */
@media (max-width: 40em) {
  .app-shell > main.app-main { padding: var(--space-lg) var(--space-md); }
}

.app-main > section.modules-page,
.app-main > section.fields-page {
  max-width: 76em;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
}

.modules-page__head,
.fields-page__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  flex-wrap: wrap;
}

.modules-page__title,
.fields-page__title {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.modules-page__title > h2,
.fields-page__title > h2 { margin: 0; }

.modules-page__count,
.fields-page__count {
  font-size: var(--text-xs);
  color: var(--text-secondary);
  background: var(--muted);
  border: 1px solid var(--border-default);
  border-radius: 1em;
  padding: 0.125em 0.5em;
}

.module-cards {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  /* 17em min: below that a card's stats row wraps awkwardly. auto-fit collapses
     empty tracks so a single card doesn't stretch across the whole row. */
  grid-template-columns: repeat(auto-fit, minmax(17em, 1fr));
  gap: var(--space-md);
}

.module-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-lg);
  background: var(--bg-panel);
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
  text-decoration: none;
  color: var(--text-primary);
  /* [TARGETED: 2026-07-29] Grid items default to min-width: auto, so an
     unbreakable child (a long translated .pill) can force this card wider
     than its grid column's max (minmax(17em, 1fr) on .module-card grid),
     pushing the card itself out of its container. min-width: 0 lets the
     card shrink to its column and pass that real width down to children,
     which is what actually makes .pill's own wrap/break-word rules apply. */
  min-width: 0;
}

.module-card:hover {
  border-color: var(--border-medium);
  transform: translateY(-2px);
  box-shadow: 0 0.5em 1.5em rgba(0, 0, 0, 0.08);
  opacity: 1;   /* override the global a:hover fade */
}

/* The card is a single <a> link, so the Translate action cannot live inside it
   (nested interactive). The <li> is the positioning context and the button
   overlays the card's bottom-right corner — the stats row is bottom-left, so
   the corner is free — sitting on top of the link, not within it. */
.module-card-wrap { position: relative; }
.module-card__translate {
  position: absolute;
  right: var(--space-sm);
  bottom: var(--space-sm);
  z-index: 1;
  background: var(--bg-panel);
  border: 1px solid var(--border-default);
  color: var(--text-secondary);
}
.module-card__translate:hover {
  border-color: var(--border-medium);
  color: var(--text-primary);
}

.module-card__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-sm);
}

.module-card__name {
  font-weight: 600;
  font-size: var(--text-md);
  line-height: 1.2;
}

.module-card__key {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--text-xs);
  color: var(--text-secondary);
  word-break: break-all;
}

.module-card__icon {
  color: var(--natural-slate);
  flex-shrink: 0;
}

/* Stat row: the "summary" the card exists for. */
.module-card__stats {
  display: flex;
  gap: var(--space-lg);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--border-light);
  margin-top: auto;   /* pins stats to the bottom so cards align in the grid */
}

.module-card__stat {
  display: flex;
  flex-direction: column;
  gap: 0.125em;
}

.module-card__stat-value {
  font-size: var(--text-lg);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.module-card__stat-label {
  font-size: var(--text-xs);
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.module-card__badges { display: flex; flex-wrap: wrap; gap: var(--space-xs); }

.pill {
  display: inline-flex;
  align-items: center;
  gap: 0.25em;
  font-size: var(--text-xs);
  border: 1px solid var(--border-default);
  border-radius: 1em;
  padding: 0.125em 0.5em;
  color: var(--text-secondary);
  /* [TARGETED: 2026-07-29] CONSTRAINT FIX — a long translated field label/value
     pair (e.g. French "Capacité de production: Bespoke, CMM verified") could
     push a pill wider than its card, forcing the CARD itself to overflow its
     grid column. white-space: nowrap alone assumed short English badge text;
     it never held for every language. max-width caps the pill to its own
     container's width (never wider than the card that holds it), and the text
     inside wraps onto multiple lines instead of forcing horizontal overflow. */
  max-width: 100%;
  white-space: normal;
  overflow-wrap: break-word;
  word-break: break-word;
}

.pill--public { background: var(--status-ok-bg); color: var(--status-ok-fg); border-color: var(--status-ok-bg); }
.pill--private { background: var(--muted); }
.pill--inactive { background: var(--status-bad-bg); color: var(--status-bad-fg); border-color: var(--status-bad-bg); }

/* Modal — native <dialog>, so Esc-to-close and focus trapping come free from
   the browser rather than being reimplemented in JS. */
.modal {
  border: none;
  border-radius: var(--radius);
  padding: 0;
  background: transparent;
  max-width: min(34em, calc(100vw - 2em));
  width: 100%;
}

.modal::backdrop { background: rgba(0, 0, 0, 0.45); }

.modal__card {
  display: flex;
  flex-direction: column;
  background: var(--bg-panel);
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
  overflow: hidden;
}

.modal__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-md) var(--space-lg);
  border-bottom: 1px solid var(--border-light);
  background: var(--muted);
}

.modal__head > h3 { margin: 0; }

.modal__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  padding: var(--space-lg);
  /* Long forms must scroll inside the dialog, never push it off-screen. */
  max-height: 70vh;
  overflow-y: auto;
}

.modal__foot {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-sm);
  padding: var(--space-md) var(--space-lg);
  border-top: 1px solid var(--border-light);
}

/* ===========================================================================
   CARD-TABLE — a table on desktop, cards on a phone. Pure CSS, no JS.
   ===========================================================================
   Each <td> carries data-label; below the breakpoint the header row is hidden
   and ::before echoes that label inside the cell, so the column header travels
   with its value instead of being lost. display:block strips the implicit table
   semantics a screen reader relies on, so the markup carries explicit
   role="row"/"cell" that survive the change.
   =========================================================================== */

.card-table-wrap { overflow-x: auto; }

.card-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}

.card-table th,
.card-table td {
  padding: var(--space-md);
  text-align: start;
  border-bottom: 1px solid var(--border-light);
  vertical-align: top;
}

.card-table > thead > tr > th {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
  background: var(--muted);
  border-bottom: 1px solid var(--border-default);
  white-space: nowrap;
}

.card-table > tbody > tr:hover { background: var(--muted); }
.card-table .cell-actions { text-align: end; white-space: nowrap; }

.card-table__name { font-weight: 500; }
.card-table__key {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--text-xs);
  color: var(--text-secondary);
  word-break: break-all;
}

.type-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.375em;
  font-size: var(--text-xs);
  border: 1px solid var(--border-default);
  border-radius: 0.25em;
  padding: 0.125em 0.5em;
  background: var(--muted);
  white-space: nowrap;
}

.type-chip > svg { width: 0.875em; height: 0.875em; color: var(--natural-slate); flex-shrink: 0; }

.rule-pills { display: flex; flex-wrap: wrap; gap: var(--space-xs); }
.rule-pill {
  font-size: var(--text-xs);
  border: 1px solid var(--border-default);
  border-radius: 0.25em;
  padding: 0 0.375em;
  color: var(--text-secondary);
  white-space: nowrap;
}
.rule-pill--req { color: var(--status-bad-fg); border-color: var(--status-bad-fg); }
.rule-pill--none { color: var(--text-secondary); opacity: 0.6; border: none; padding: 0; }

/* --- Field reorder: drag handle + up/down buttons (admin-fields.ejs). Desktop
   drags the row; mobile (no native drag support) uses the arrow buttons — both
   paths write the SAME sort_order, so neither is a "lesser" fallback. --- */
.field-order {
  display: inline-flex;
  align-items: center;
  gap: 0.25em;
}
.field-order__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75em;
  height: 1.75em;
  padding: 0;
  flex-shrink: 0;
  background: var(--bg-panel);
  border: 1px solid var(--border-default);
  border-radius: 0.375em;
  color: var(--text-secondary);
  cursor: pointer;
  transition: background-color 0.15s ease-out, border-color 0.15s ease-out, color 0.15s ease-out;
}
.field-order__btn > svg { width: 0.875em; height: 0.875em; }
.field-order__btn:hover:not(:disabled) { background: var(--bg-page); border-color: var(--border-medium); color: var(--text-primary); }
.field-order__btn:focus-visible { outline: 0.125em solid var(--primary); outline-offset: 0.125em; }
.field-order__btn:disabled { opacity: 0.35; cursor: not-allowed; }

.field-order__handle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75em;
  height: 1.75em;
  color: var(--natural-slate);
  cursor: grab;
}
.field-order__handle > svg { width: 1em; height: 1em; }

/* Desktop: drag is the primary path, so the handle shows and the arrow buttons
   step back to a secondary (still-usable) role. Mobile: no native HTML5 drag
   support, so the handle hides and the arrows carry the whole feature. */
@media (max-width: 52em) {
  .field-order__handle { display: none; }
}
@media (min-width: 52.01em) {
  .cell-order { width: 1%; white-space: nowrap; }
}

.card-table > tbody > tr.is-dragging { opacity: 0.4; }
.card-table > tbody > tr.is-drop-target { background: var(--muted); outline: 0.125em dashed var(--border-medium); outline-offset: -0.125em; }

@media (max-width: 52em) {
  .card-table > thead { display: none; }
  .card-table,
  .card-table > tbody,
  .card-table > tbody > tr,
  .card-table td { display: block; width: 100%; }

  .card-table > tbody > tr {
    border: 1px solid var(--border-default);
    border-radius: var(--radius);
    margin-bottom: var(--space-md);
    padding: var(--space-sm);
    background: var(--bg-panel);
  }

  .card-table td {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
    border-bottom: 1px solid var(--border-light);
    padding: var(--space-sm) var(--space-md);
  }

  .card-table > tbody > tr > td:last-child { border-bottom: none; }

  .card-table td[data-label]::before {
    content: attr(data-label);
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-secondary);
    flex-shrink: 0;
  }

  /* The name cell is the card's heading: full width, no label prefix. */
  .card-table td.cell-name {
    display: block;
    background: var(--muted);
    border-radius: 0.25em;
    margin-bottom: var(--space-xs);
  }
  .card-table td.cell-name::before { content: none; }
  .card-table .cell-actions { justify-content: flex-end; }
}

/* Fields page: the module picker is the page's context, so it reads as part of
   the heading rather than a form control bolted above it. */
.fields-page__picker-label {
  font-size: var(--text-xs);
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-weight: 600;
}

.fields-page__picker { min-width: 12em; max-width: 20em; flex: 1; }
.fields-page__picker > select { width: 100%; }

.module-summary {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  padding: var(--space-md);
  background: var(--muted);
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
  font-size: var(--text-xs);
  color: var(--text-secondary);
  align-items: center;
}

.option-list { display: flex; flex-wrap: wrap; gap: var(--space-xs); }
.option-list:empty::before {
  content: 'No options yet — this field will reject every submission until you add some.';
  font-size: var(--text-xs);
  color: var(--status-bad-fg);
}

.option-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--text-xs);
  border: 1px solid var(--border-default);
  border-radius: 0.25em;
  padding: 0.25em 0.5em;
  background: var(--bg-panel);
}

.option-chip > button {
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
  line-height: 0;
  color: var(--text-secondary);
}
.option-chip > button:hover { color: var(--natural-red); transform: scale(1.15); }

.option-add { display: flex; gap: var(--space-sm); align-items: center; }
.option-add > input { flex: 1; }
.option-add > .btn { flex-shrink: 0; }

/* ===========================================================================
   PUBLIC SITE — homepage, browse, detail
   ===========================================================================
   Reuses .module-cards (auto-fit/minmax) so the public grid reflows itself the
   same way the admin one does — one card system, no second breakpoint set.
   =========================================================================== */

.public-shell { min-height: 100vh; display: flex; flex-direction: column; }

.public-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-md) var(--space-lg);
  background: var(--bg-panel);
  border-bottom: 1px solid var(--border-default);
  position: relative;
}

.public-header__brand { display: flex; align-items: center; gap: var(--space-sm); min-width: 0; }
.public-header__brand > .icon { color: var(--primary); flex-shrink: 0; }
.public-header__brand > strong { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.public-header__home { display: inline-flex; color: var(--primary); }
.public-header__home:hover { opacity: 1; }

.public-main {
  flex: 1;
  width: 100%;
  max-width: 76em;
  margin: 0 auto;
  padding: var(--space-xl) var(--space-lg);
  display: flex;
  flex-direction: column;
  gap: var(--space-xl);
}

.public-hero { display: flex; flex-direction: column; gap: var(--space-sm); }
.public-hero > h1 { margin: 0; }
.public-hero--compact { gap: var(--space-xs); }
.public-hero__tagline { color: var(--text-secondary); margin: 0; max-width: 44em; }

.public-section { display: flex; flex-direction: column; gap: var(--space-md); }

.public-section__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-md);
  flex-wrap: wrap;
  padding-bottom: var(--space-sm);
  border-bottom: 1px solid var(--border-light);
}

.public-section__head > div > h2 { margin: 0; }
.public-section__desc { margin: var(--space-xs) 0 0; color: var(--text-secondary); font-size: var(--text-sm); max-width: 44em; }

/* Clamp the blurb so cards in a row stay the same height regardless of how
   much text an admin wrote. */
.public-card__lead {
  color: var(--text-secondary);
  font-size: var(--text-sm);
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.public-directory { display: flex; flex-direction: column; gap: var(--space-md); }
.public-directory > h2 { margin: 0; }

.public-footer {
  padding: var(--space-lg);
  border-top: 1px solid var(--border-default);
  color: var(--text-secondary);
  font-size: var(--text-xs);
  text-align: center;
}

.breadcrumb {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--text-xs);
  color: var(--text-secondary);
}
.breadcrumb > a { color: var(--text-secondary); text-decoration: none; }
.breadcrumb > a:hover { color: var(--text-primary); opacity: 1; }

.detail-list { margin: 0; display: flex; flex-direction: column; gap: var(--space-sm); }
.detail-list { gap: var(--space-xs); }

/* Rounded, padded rows rather than bare border-separated text. Each row is a
   discrete fact, so it reads as a card — matching .module-card, which uses the
   same --radius token. */
.detail-list__row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-xs);
  padding: var(--space-md);
  background: var(--muted);
  border: 1px solid var(--border-light);
  border-radius: var(--radius);
}

.detail-list__row:hover { border-color: var(--border-default); }
@media (min-width: 34em) {
  .detail-list__row { grid-template-columns: 10em 1fr; gap: var(--space-md); }
}
.detail-list__row > dt {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
  font-weight: 600;
}
.detail-list__row > dd {
  margin: 0;
  /* A supplier's website URL has no spaces, so a normal line break cannot
     happen — it pushes the whole modal wider than a phone screen and the page
     scrolls sideways. overflow-wrap breaks it only when it would otherwise
     overflow; word-break: break-all would chop every long word, which is worse.
     min-width:0 is required for a grid child to be allowed to shrink at all. */
  min-width: 0;
  overflow-wrap: anywhere;
}

/* Cards carry the same risk: a long unbroken value would widen the grid track
   and break the auto-fit columns. */
.module-card__key,
.card-table__key,
.public-card__lead { overflow-wrap: anywhere; min-width: 0; }

/* The depth prompt: says how much more exists, never shows it. */
.deeper-prompt {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  flex-wrap: wrap;
  padding: var(--space-md);
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
  background: var(--muted);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}
.deeper-prompt > .icon { color: var(--natural-slate); flex-shrink: 0; }
.deeper-prompt > span { flex: 1; min-width: 10em; }

/* [TARGETED: 2026-07-19] Talk-to-the-supplier CTA — the tempting, prominent
   action on a service page. em-based, natural colours, subtle motion. */
.contact-cta {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-xs);
  margin-top: var(--space-lg);
  padding: var(--space-lg);
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
  background: var(--bg-panel);
}
.contact-cta__btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  font-size: var(--text-md);
  padding: 0.6em 1.2em;
  transition: transform 0.15s ease-out, box-shadow 0.15s ease-out, filter 0.15s ease-out;
}
.contact-cta__btn:hover {
  transform: translateY(-0.125em);
  box-shadow: 0 0.25em 0.75em rgba(0, 0, 0, 0.12);
  filter: brightness(1.03);
}
.contact-cta__btn:active { transform: translateY(0) scale(0.99); }
.contact-cta__btn.is-busy { opacity: 0.7; pointer-events: none; }
.contact-cta__btn > .icon { width: 1.15em; height: 1.15em; }
.contact-cta__hint {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--text-secondary);
}

/* Public Site admin: settings rows + the anonymous preview. */

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

.setting-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  flex-wrap: wrap;
  padding: var(--space-md);
  border: 1px solid var(--border-default);
  border-radius: 0.375em;
}

.setting-row:hover { border-color: var(--border-medium); }
.setting-row__info { display: flex; flex-direction: column; gap: 0.125em; flex: 1; min-width: 12em; }
.setting-row__label { font-weight: 500; }
.setting-row__desc { font-size: var(--text-xs); color: var(--text-secondary); max-width: 40em; }
.setting-row__control { min-width: 12em; max-width: 20em; flex: 1; }
/* A translatable setting stacks one input per language, so its control needs
   room to breathe — it overrides the 20em cap the single-value controls use. */
.setting-row__control:has(.setting-i18n) { max-width: 28em; }

/* [TARGETED: 2026-07-24] head-snippet — a raw-HTML analytics setting. It needs the
   full row width (a <script> tag does not fit a 20em box) and a multi-line editor,
   so the row stacks vertically and the control spans full width. */
.setting-row--wide {
  flex-direction: column;
  align-items: stretch;
}
.setting-row--wide > .setting-row__control {
  max-width: none;
  min-width: 0;
  width: 100%;
}
.setting-snippet { display: flex; flex-direction: column; gap: var(--space-sm); }
.setting-snippet__input {
  width: 100%;
  min-height: 4.5em;
  resize: vertical;
  padding: var(--space-sm) var(--space-md);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--text-primary);
  background: var(--bg-page);
  border: 1px solid var(--border-default);
  border-radius: 0.375em;
  transition: border-color var(--duration-fast) var(--ease-standard),
              box-shadow var(--duration-fast) var(--ease-standard);
}
.setting-snippet__input:hover { border-color: var(--border-medium); }
.setting-snippet__input:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 0.1875em color-mix(in srgb, var(--primary) 18%, transparent);
}
/* Admin-only caution line — natural amber, full border, never a neon accent. */
.setting-snippet__note {
  display: flex;
  align-items: flex-start;
  gap: var(--space-xs);
  margin: 0;
  padding: var(--space-sm) var(--space-md);
  font-size: var(--text-xs);
  line-height: 1.5;
  color: var(--natural-amber);
  background: color-mix(in srgb, var(--natural-amber) 8%, transparent);
  border: 1px solid color-mix(in srgb, var(--natural-amber) 35%, transparent);
  border-radius: 0.375em;
}
.setting-snippet__icon {
  width: 1em;
  height: 1em;
  flex: 0 0 auto;
  margin-top: 0.1em;
}

/* Per-language editor for translatable settings (site title / tagline). One
   labelled row per active language; the language chip sits before its input and
   mirrors for RTL via the logical border + the row's dir attribute. */
.setting-i18n { display: flex; flex-direction: column; gap: var(--space-xs); }
.setting-i18n__row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}
.setting-i18n__lang {
  flex: 0 0 4.5em;
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--text-secondary);
  text-align: start;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.setting-i18n__row input {
  flex: 1;
  min-width: 0;
}
/* The base (en) row reads as the source: a slightly stronger label so an admin
   sees at a glance which box feeds every untranslated language. */
.setting-i18n__row:first-child .setting-i18n__lang { color: var(--text-primary); }

/* Content translation modal (module name/description, field label/help): each
   translatable field gets a labelled section stacking one setting-i18n editor. */
.translate-section { margin-bottom: var(--space-lg); }
.translate-section:last-child { margin-bottom: 0; }
.translate-section__title {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
  margin: 0 0 var(--space-sm);
}

/* [TARGETED: 2026-07-20] AI-translate — the "AI" button sits beside its
   section title rather than under it, so pressing it reads as acting ON that
   section (label / help / name / category), not as a separate unrelated
   control floating in the modal body. */
.translate-section__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  margin-bottom: var(--space-sm);
}
.translate-section__head > .translate-section__title { margin-bottom: 0; }

.ai-translate-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.375em;
  font-size: var(--text-xs);
  padding: 0.25em 0.625em;
  color: var(--primary);
  border-color: var(--primary);
  flex-shrink: 0;
}
.ai-translate-btn > .icon { width: 1em; height: 1em; }
.ai-translate-btn:hover { background: var(--muted); }

/* AI-fill status line — mirrors .msg's is-ok/is-error, adds a neutral
   "in progress" state the plain .msg component doesn't have. */
.msg.is-busy { color: var(--text-secondary); }

/* Preview pane: a compact mirror of the anonymous payload, not a real render —
   it shows WHICH fields ship, which is the thing being decided here. */
.preview-site {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  padding: var(--space-lg);
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
  background: var(--bg-page);
}

.preview-site__title { font-size: var(--text-lg); font-weight: 600; }
.preview-site__tagline { color: var(--text-secondary); font-size: var(--text-sm); }

.preview-section {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: var(--space-md);
  border: 1px solid var(--border-light);
  border-radius: 0.375em;
  background: var(--bg-panel);
}

.preview-section__head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-sm); flex-wrap: wrap; }
.preview-fields { display: flex; flex-wrap: wrap; gap: var(--space-xs); }

/* ===========================================================================
   TOGGLE SWITCH (.visibility-toggle)
   ===========================================================================
   Lives here, not in a page's <style>, because it is a component — settings.html
   had the only copy and nothing else could reuse it.

   Three bugs it fixes, all measured:
   1. The knob was INVISIBLE. Track used --border-default and knob used
      --bg-panel: 1.44:1 in light, 1.30:1 in dark. Even the checked state was
      2.28:1 on --natural-green. WCAG 1.4.11 requires 3:1 for a UI component,
      so all four states failed. --switch-on / --switch-knob are tuned per
      scheme; every state now measures >= 3:1.
   2. The knob had NO BOX. The track had no display/position and the knob is a
      <span> — an inline element ignores width/height entirely, so nothing was
      drawn and translateX had nothing to move.
   3. No hover, no focus ring. The <input> is visually hidden, so without a
      :focus-visible rule on the track a keyboard user got no indication at all
      (base.css sets *:focus { outline: none }).
   =========================================================================== */

.visibility-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  cursor: pointer;
  user-select: none;
  flex-shrink: 0;
}

/* Visually hidden, not display:none — a display:none input is unfocusable and
   drops out of the tab order, so the switch would be keyboard-dead. */
.visibility-toggle > input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
  margin: 0;
}

.visibility-toggle__track {
  /* display:flex is what gives the knob a box at all (bug 2). */
  display: flex;
  align-items: center;
  width: 2.5em;
  height: 1.375em;
  padding: 0.125em;
  background: var(--border-dark);
  border: 1px solid transparent;   /* reserved so :focus-visible doesn't shift layout */
  border-radius: 1em;
  flex-shrink: 0;
  transition: background 0.2s ease-out, border-color 0.15s ease-out;
}

.visibility-toggle__knob {
  display: block;
  width: 1.125em;
  height: 1.125em;
  background: var(--switch-knob);
  border-radius: 50%;
  box-shadow: 0 0.0625em 0.1875em rgba(0, 0, 0, 0.3);
  transition: transform 0.2s ease-out;
}

.visibility-toggle:hover > .visibility-toggle__track {
  border-color: var(--border-medium);
  filter: brightness(1.08);
}

.visibility-toggle > input:checked + .visibility-toggle__track {
  background: var(--switch-on);
}

.visibility-toggle > input:checked + .visibility-toggle__track > .visibility-toggle__knob {
  transform: translateX(1.125em);
}

/* :focus-visible, not :focus — a mouse click should not draw a ring, but
   keyboard navigation must. outline (not box-shadow) survives Windows High
   Contrast mode, which discards shadows. */
.visibility-toggle > input:focus-visible + .visibility-toggle__track {
  outline: 0.1875em solid var(--primary);
  outline-offset: 0.125em;
}

.visibility-toggle > input:disabled + .visibility-toggle__track {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Row that pairs a field with its toggle. align-items:center, not flex-end:
   the toggle is a fixed-height control and should sit against the label, not
   be dragged down by however tall the field beside it happens to be. */
.field-with-toggle {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.field-with-toggle > .form-group {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.visibility-toggle__label {
  font-size: var(--text-xs);
  color: var(--text-secondary);
  display: flex;
  align-items: center;
  gap: 0.25em;
}

/* ===========================================================================
   PAGE CONTROLS — language + theme, shoulder to shoulder, above nothing
   ===========================================================================
   ONE container owns positioning; the children own NONE. The previous design
   let each control position itself and then tried to wrap them afterwards —
   the wrap silently failed, so both fell into normal flow and rendered ON TOP
   of the page content.

   Two placements, one component:
     --inline : a flow item inside a header row (admin pages, settings, account)
     --corner : floats top-end on header-less pages (login, register, browse)
   =========================================================================== */

.page-controls {
  display: flex;
  align-items: center;
  /* Shoulder to shoulder: no gap. The children butt against each other and a
     single hairline separates them (see below) — a gap would read as two
     unrelated widgets. */
  gap: 0;
  background: var(--bg-panel);
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
  /* The cluster is the ONLY thing that draws chrome. Children are transparent. */
  /* [TARGETED: 2026-07-16] overflow:hidden REMOVED — it was clipping the dropdown.
     It existed only to round the children against the cluster's corners. But in
     --corner mode the cluster is position:absolute, which makes it the dropdown's
     CONTAINING BLOCK as well as its clipper — so the language menu was trapped
     inside the cluster's own ~7em box and effectively invisible.
     Worst on a phone: the cluster is pinned to the screen edge, so an 11em menu
     clipped to ~7em left almost nothing usable. Reported as "in mobile i cannot
     change language".
     The rounding is now done by the first/last children directly (below), which
     needs no clipping and therefore traps nothing. */
  flex-shrink: 0;
  width: max-content;
}

/* The rounding overflow:hidden used to provide. Only the outer edges: the join
   between two controls must stay square, or the hairline separator would sit in
   a gap between two rounded corners.
   Logical properties (start/end, not left/right) so this mirrors under RTL —
   which matters here, because the control that FLIPS the page to RTL is in this
   very cluster. */
.page-controls > :first-child {
  border-start-start-radius: var(--radius);
  border-end-start-radius: var(--radius);
}

.page-controls > :last-child {
  border-start-end-radius: var(--radius);
  border-end-end-radius: var(--radius);
}

.page-controls:hover { border-color: var(--border-medium); }

/* Header pages: a normal flow item. No positioning at all — the header's flex
   row places it, so it can never overlap a sibling. */
.page-controls--inline { position: static; }

/* Header-less pages: float in the top-end corner.
   z-index 20 sits above page content but BELOW the mobile nav panel (z-index
   20 in .app-nav) and below a <dialog>, which the browser puts in the top
   layer — so it can never cover a modal or an open menu. */
/* [TARGETED: 2026-07-17] The corner cluster is IN FLOW, not floating over content.
//
   THE BUG: this was `position: absolute; top: var(--space-lg)`. Absolute means
   OUT OF FLOW — nothing reserves space for it — so on the 6 public pages with no
   header (login, register, account, settings, profile, error) it was painted
   directly ON TOP of the page's own content. On login that is the auth card:
   .auth-page is min-height:100vh and centres its card, so the cluster covered the
   logo and the "Sign In" heading. Reported as "covering valuable contents".
   z-index only decided WHAT it covered, never WHETHER.

   THE FIX: a real bar in the document flow. The page's content starts BELOW it
   because the bar occupies its own line — which is what "reserving space" means.
   No z-index games, no per-page padding to compensate, nothing to keep in sync.

   sticky, not absolute: it stays reachable when a long page scrolls (settings,
   account) without ever leaving the flow. `top: 0` is the sticky anchor, not a
   position — the bar still takes its own height at the top of the page. */
.page-controls--corner {
  position: sticky;
  top: 0;
  z-index: 20;

  /* THE LINE THAT FIXES THE OVERLAP: as a flow item it must not be
     width:max-content (the base rule) or it would sit inline with content.
     Full width = its own row = content starts underneath it. */
  width: 100%;
  display: flex;
  /* flex-end + logical margin: the controls stay in the end corner (right in
     LTR, left in RTL) while the BAR spans the full width. */
  justify-content: flex-end;

  /* The bar itself draws no chrome — the cluster's own border/background/radius
     from the base rule would otherwise become a full-width slab. */
  background: none;
  border: none;
  border-radius: 0;
  box-shadow: none;
  padding: var(--space-md) var(--space-lg);
  /* box-sizing so the padding does not push the bar past 100% and create a
     horizontal scrollbar. */
  box-sizing: border-box;

  /* A translucent backdrop so the controls stay legible when a long page
     (settings, account) scrolls content underneath the sticky bar. */
  background: color-mix(in oklch, var(--bg-page) 85%, transparent);
  backdrop-filter: blur(0.25em);
}

/* Children: no chrome of their own, no positioning, no radius. The cluster's
   overflow:hidden clips them to its own rounded corners, so the ends are
   rounded and the join is square — with no per-child radius rules. */
.page-controls > * {
  position: static;
  border: none;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}

/* The hairline join. :not(:first-child) is the standard border-collapse trick —
   one border between neighbours, never two, and none on the outer edges. */
.page-controls > * + * {
  border-inline-start: 1px solid var(--border-light);
}

/* Both controls share one height and hit area so they read as one unit.
   [TARGETED: 2026-07-20] UI-0001 BUG FOUND AND FIXED DURING LIVE TESTING:
   every rule in this block through .lang-switcher__option.is-active below was
   scoped ONLY to `.page-controls > .lang-switcher > ...`. absorbPageControls()
   (public-menu.js) relocates `.lang-switcher` OUT of `.page-controls` into
   `.account-panel__prefs-host` — so once relocated, none of these selectors
   matched any more. Confirmed live with a real headless-browser test: the
   language dropdown still opened (`.is-open` toggling is unaffected — that is
   JS, not CSS) but rendered as 10 UNSTYLED bare rows with no background, no
   border, no position:absolute, no max-height/overflow-y clamp — the EXACT
   historical bug this block's own original comment describes fixing once
   already ("10 unstyled language names floated over the page with no panel
   behind them"). Caught by opening the panel and reading computed styles, not
   assumed fixed by construction.

   FIX: add `.account-panel__prefs-host > .lang-switcher > ...` as a second,
   equally-scoped selector alongside the original `.page-controls > ...` one on
   every affected rule — comma-grouped, so admin pages (which still use the
   classic `.page-controls` cluster, untouched) keep exactly the styling they
   had, and the relocated public-page switcher gets the identical styling in
   its new location. Nothing here changes behavior for `.page-controls`. */
.page-controls > .theme-toggle,
.page-controls > .lang-switcher > .lang-switcher__trigger,
.account-panel__prefs-host > .lang-switcher > .lang-switcher__trigger {
  border: none;
  border-radius: 0;
  background: transparent;
  padding: 0.5em 0.75em;
  min-height: 2.25em;
}

.page-controls > .theme-toggle:hover,
.page-controls > .lang-switcher > .lang-switcher__trigger:hover,
.account-panel__prefs-host > .lang-switcher > .lang-switcher__trigger:hover {
  background: var(--muted);
}

/* Focus ring must sit ABOVE the neighbour's border, or the cluster's
   overflow:hidden clips it and a keyboard user sees a half-drawn ring. */
.page-controls > .theme-toggle:focus-visible,
.page-controls > .lang-switcher > .lang-switcher__trigger:focus-visible,
.account-panel__prefs-host > .lang-switcher > .lang-switcher__trigger:focus-visible {
  outline: 0.125em solid var(--primary);
  outline-offset: -0.125em;
  position: relative;
  z-index: 1;
}

/* The switcher's dropdown must escape the cluster's overflow:hidden clip. */
.page-controls > .lang-switcher,
.account-panel__prefs-host > .lang-switcher { position: static; }
/* [TARGETED: 2026-07-16] The language dropdown MENU — it had no styling at all.
   Reported as "the dropdown does not show". It was never hidden; it was
   INVISIBLE and always open:
     * no `.lang-switcher.is-open` rule existed, so the menu was display:block
       permanently — the JS toggles .is-open on the container and NOTHING in CSS
       ever listened for it. (The one `is-open` rule in this file is
       `.app-nav.is-open`, the mobile nav — unrelated, and the reason a grep for
       "is-open" looked like it was handled.)
     * background computed to rgba(0,0,0,0) — transparent
     * no border, no padding, min-width:auto
   So 10 unstyled language names floated over the page with no panel behind them.

   Scoped to .page-controls > .lang-switcher > … to match the existing rule this
   replaces, PLUS .account-panel__prefs-host > .lang-switcher > … (2026-07-20,
   UI-0001) for the relocated public-page switcher — see the block comment
   above for why the second scope is necessary, not decorative. */
.page-controls > .lang-switcher > .lang-switcher__menu,
.account-panel__prefs-host > .lang-switcher > .lang-switcher__menu {
  position: absolute;
  top: calc(100% + 0.25em);
  inset-inline-end: 0;
  z-index: 30;

  /* HIDDEN by default. This is the actual fix for "does not show": the menu was
     permanently visible, so there was no closed state to open FROM. */
  display: none;

  /* Chrome matching .modal__card — the established panel look in this file,
     not a new invention. Full border, natural colour, never a one-sided accent. */
  background: var(--bg-panel);
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
  box-shadow: 0 0.25em 0.75em rgba(0, 0, 0, 0.12);
  padding: var(--space-xs);

  /* Wide enough that "English / العربية" never wraps mid-word, capped so a long
     native name cannot push it off a phone screen. */
  min-width: 11em;
  max-width: min(16em, calc(100vw - 2em));

  /* 10 languages on a short screen must scroll inside the menu rather than
     overflow the viewport with no way to reach the last option. */
  max-height: min(60vh, 20em);
  overflow-y: auto;
}

/* The open state the JS has been toggling all along, unheard until now. */
.page-controls > .lang-switcher.is-open > .lang-switcher__menu,
.account-panel__prefs-host > .lang-switcher.is-open > .lang-switcher__menu {
  display: block;
  animation: fadeIn 0.15s ease-out;
}

/* Each language row. These had NO CSS whatsoever: bare <div>s with no padding
   and cursor:auto — no hit area and no affordance that they were clickable. */
.page-controls > .lang-switcher > .lang-switcher__menu > .lang-switcher__option,
.account-panel__prefs-host > .lang-switcher > .lang-switcher__menu > .lang-switcher__option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  /* A real touch target. Bare text rows were ~1.2em tall — under any reasonable
     minimum, and unusable on a phone. */
  padding: var(--space-sm) var(--space-md);
  border-radius: 0.25em;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color 0.15s ease-out, color 0.15s ease-out;
}

.page-controls > .lang-switcher > .lang-switcher__menu > .lang-switcher__option:hover,
.account-panel__prefs-host > .lang-switcher > .lang-switcher__menu > .lang-switcher__option:hover {
  background: var(--bg-page);
}

/* The active language. Weight + colour, never colour alone — colour alone fails
   for anyone who cannot distinguish it. */
.page-controls > .lang-switcher > .lang-switcher__menu > .lang-switcher__option.is-active,
.account-panel__prefs-host > .lang-switcher > .lang-switcher__menu > .lang-switcher__option.is-active {
  font-weight: 600;
  color: var(--primary);
}

/* The native name, secondary to the English name beside it. */
.lang-switcher__option-native {
  color: var(--text-secondary);
  font-size: var(--text-sm);
  /* dir=auto so an Arabic or Urdu native name renders RTL inside an LTR menu —
     without it the text reorders wrongly next to the Latin name. */
  unicode-bidi: isolate;
}

/* Inside a header the cluster must not be clipped, or the dropdown vanishes. */
.app-header { overflow: visible; }

.fade-in {
  animation: fadeIn 0.3s ease-out;
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(0.5em); }
  to { opacity: 1; transform: translateY(0); }
}

/* ===========================================================================
   [TARGETED: 2026-07-17] RECORD IMAGES — figure, gallery, placeholder.
   Simple on purpose: a frame, an aspect ratio, and a loading state. Nothing
   here is decorative; every rule prevents a specific visible fault.
   =========================================================================== */

/* The frame. aspect-ratio is the load-bearing rule: without it the box has zero
   height until the image arrives, so the page renders, then JUMPS as each image
   lands. Reserving the space up front is what stops that (it is what Core Web
   Vitals measures as layout shift). */
.record-image {
  position: relative;
  display: block;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--bg-page);
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
}

/* object-fit: cover, never plain width/height: a 4:3 frame given a 16:9 photo
   would otherwise squash it. cover crops instead of distorting — a stretched
   face is worse than a cropped one.
   display:block kills the ~4px inline-gap under an <img>, which would show as a
   hairline of background inside the frame. */
.record-image > img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Fade in as it decodes, so a slow image arrives softly instead of snapping. */
  animation: fadeIn 0.25s ease-out;
}

/* THE PLACEHOLDER. Shown while loading and when there is no image at all.
   A quiet shimmer, not a spinner: it occupies exactly the frame the image will
   fill, so nothing moves when the real thing replaces it. */
.record-image--empty,
.record-image--loading {
  /* Three stops of the SAME two tokens — a sheen passing over the panel colour,
     not a rainbow. Natural palette, no neon. */
  background-image: linear-gradient(
    100deg,
    var(--bg-page) 30%,
    var(--bg-panel) 50%,
    var(--bg-page) 70%
  );
  background-size: 200% 100%;
  animation: imageShimmer 1.4s ease-in-out infinite;
}

@keyframes imageShimmer {
  from { background-position: 200% 0; }
  to   { background-position: -200% 0; }
}

/* Anyone who has asked for less motion gets a still panel. The shimmer is
   decoration; for a vestibular disorder it is a symptom trigger. */
@media (prefers-reduced-motion: reduce) {
  .record-image--empty,
  .record-image--loading { animation: none; background-image: none; }
  .record-image > img { animation: none; }
}

/* The "no image" mark, centred in the frame. Only inside an EMPTY placeholder —
   a loading frame must not show a broken-image icon before the real one lands. */
.record-image--empty::after {
  content: '';
  position: absolute;
  inset: 0;
  margin: auto;
  width: 2.5em;
  height: 2.5em;
  opacity: 0.28;
  /* A 1px-stroke outline mark, matching every SVG in this app. Inline as a
     data URI because a placeholder must not depend on a network request that
     may be the very thing that is failing. currentColor cannot cross into a
     background image, so the stroke is a literal token-matched grey. */
  background: no-repeat center / contain
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='3' width='18' height='18' rx='2'/%3E%3Ccircle cx='8.5' cy='8.5' r='1.5'/%3E%3Cpath d='M21 15l-5-5L5 21'/%3E%3C/svg%3E");
}

/* THE GALLERY. auto-fill + minmax, not a fixed column count: the grid reflows
   from 1 column on a phone to as many as fit, with no media query and no
   breakpoint to maintain. */
.record-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(12em, 100%), 1fr));
  gap: var(--space-md);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* min(12em, 100%) matters: a bare 12em minimum would overflow a viewport
   narrower than 12em, forcing the whole page to scroll sideways. */

.record-gallery > li { margin: 0; }

/* The lead image on a detail page: wider frame, full width. */
.record-image--lead {
  aspect-ratio: 16 / 9;
  width: 100%;
  margin-bottom: var(--space-md);
}

/* A clickable thumbnail. Same lift as .module-card, so the app has ONE hover
   language rather than a new one per component. */
a.record-image {
  cursor: pointer;
  transition: transform 0.2s ease-out, box-shadow 0.2s ease-out, border-color 0.2s ease-out;
}

a.record-image:hover {
  transform: translateY(-2px);
  border-color: var(--border-medium);
  box-shadow: 0 0.25em 0.75em rgba(0, 0, 0, 0.1);
}

a.record-image:focus-visible {
  outline: 0.125em solid var(--primary);
  outline-offset: 0.125em;
}

/* ===========================================================================
   [TARGETED: 2026-07-20] HERO GALLERY — the record page's top-of-fold image
   carousel (renderHeroGallery, record-render.js). CSS scroll-snap is the
   dependency-free carousel primitive (researched: core scroll-snap properties
   below are supported in every evergreen browser; the newer
   ::scroll-marker()/::scroll-button() pseudo-elements are Chrome/Safari-only
   so far and are deliberately NOT relied on — the dots/arrows here are real
   elements, enhanced by record-hero-gallery.js). Degrades gracefully with zero
   JS to a swipeable/scrollable strip; the buttons and dots are simply inert
   until the script attaches, never absent or broken-looking.
   =========================================================================== */
.hero-gallery {
  position: relative;
  margin-bottom: var(--space-lg);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--bg-page);
}

.hero-gallery--single { line-height: 0; }

.hero-gallery__track {
  display: flex;
  margin: 0;
  padding: 0;
  list-style: none;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  /* A visible scrollbar under custom nav controls reads as a bug, not a
     feature — the buttons/dots are the intended affordance. */
  scrollbar-width: none;
}
.hero-gallery__track::-webkit-scrollbar { display: none; }

@media (prefers-reduced-motion: reduce) {
  /* Snapping still WORKS with reduced motion — only the smooth glide between
     positions is removed, same rule this file applies everywhere else motion
     is decorative rather than functional. */
  .hero-gallery__track { scroll-behavior: auto; }
}

.hero-gallery__slide {
  flex: 0 0 100%;
  scroll-snap-align: start;
  scroll-snap-stop: always;
  aspect-ratio: 16 / 9;
}

.hero-gallery__slide > img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  animation: fadeIn 0.25s ease-out;
}
@media (prefers-reduced-motion: reduce) { .hero-gallery__slide > img { animation: none; } }

/* Prev/next — same lifted-button language as every other icon control in this
   app (page-controls, msg-thread__action), not a new pattern. Hidden until
   record-hero-gallery.js confirms there is more than one slide to move
   between (it removes [hidden] on the multi-slide markup only). */
.hero-gallery__nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 2.5em;
  height: 2.5em;
  border-radius: 50%;
  border: 1px solid var(--border-default);
  background: color-mix(in oklch, var(--bg-panel) 88%, transparent);
  color: var(--text-primary);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: transform 0.15s ease-out, background-color 0.15s ease-out, box-shadow 0.15s ease-out;
}
.hero-gallery__nav:hover { background: var(--bg-panel); box-shadow: var(--shadow-md); }
.hero-gallery__nav:active { transform: translateY(-50%) scale(0.94); }
.hero-gallery__nav:focus-visible { outline: 0.125em solid var(--primary); outline-offset: 0.125em; }
.hero-gallery__nav > .icon { width: 1.1em; height: 1.1em; }
.hero-gallery__nav--prev { left: var(--space-md); }
.hero-gallery__nav--next { right: var(--space-md); }

/* A carousel with nothing to page through (record-hero-gallery.js measures
   scrollWidth vs clientWidth) hides its own controls rather than showing a
   prev/next pair that would do nothing. */
.hero-gallery__nav[hidden], .hero-gallery__dots[hidden] { display: none; }

.hero-gallery__dots {
  position: absolute;
  bottom: var(--space-sm);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 0.5em;
  padding: 0.375em 0.625em;
  border-radius: 999px;
  background: color-mix(in oklch, var(--bg-panel) 70%, transparent);
}

.hero-gallery__dot {
  width: 0.5em;
  height: 0.5em;
  border-radius: 50%;
  border: none;
  padding: 0;
  background: var(--border-medium);
  cursor: pointer;
  transition: transform 0.15s ease-out, background-color 0.15s ease-out;
}
.hero-gallery__dot:hover { background: var(--text-secondary); }
.hero-gallery__dot.is-active { background: var(--primary); transform: scale(1.3); }
.hero-gallery__dot:focus-visible { outline: 0.125em solid var(--primary); outline-offset: 0.125em; }

@media (max-width: 34em) {
  /* On a narrow phone the frame is closer to square: a 16:9 crop of a
     workshop-floor photo can lose the actual subject at the sides. */
  .hero-gallery__slide { aspect-ratio: 4 / 3; }
  .hero-gallery__nav { width: 2.25em; height: 2.25em; }
}

/* ===========================================================================
   [TARGETED: 2026-07-17] The record detail PAGE.
   .record-page / .record-page__title were used by public-record.ejs and NEVER
   DEFINED — the template invented them. Same phantom-class fault as
   .lang-switcher--inline: markup referencing a class the stylesheet has never
   heard of, which renders as unstyled default HTML and looks "broken" rather
   than throwing.
   =========================================================================== */

.record-page {
  /* A reading measure. Field values are prose; a full-width line on a desktop
     monitor is genuinely hard to track back to the next line's start. */
  max-width: 52em;
  margin: 0 auto;
  padding: var(--space-lg) 0;
}

.record-page__title {
  /* The h1 IS the record's own name, so it carries the page. Sized from the
     scale, never a magic number. */
  font-size: var(--text-xl);
  line-height: 1.25;
  margin: 0 0 var(--space-lg) 0;
  color: var(--text-primary);
  /* A long unbroken service name (or a German compound) must wrap rather than
     force the whole page to scroll sideways. */
  overflow-wrap: anywhere;
}

/* The image row inside the detail list: the gallery needs the full width, not
   the narrow <dd> column a text value sits in. */
.detail-list__row--images {
  display: block;
}

.detail-list__row--images > dt {
  margin-bottom: var(--space-sm);
}

.detail-list__row--images > dd {
  margin: 0;
}

/* ===========================================================================
   [TARGETED: 2026-07-17] PHANTOM CLASSES — used in markup, defined NOWHERE.
   Found by auditing every class in every template and script against every
   stylesheet INCLUDING the per-page pageStyle blocks. 20 classes were being
   applied to real elements that no rule had ever heard of. They do not throw —
   they render as unstyled default HTML, which reads as "broken CSS" rather than
   as a missing rule. This is the third time this fault has shipped
   (.lang-switcher--inline, .record-page, now these).
   =========================================================================== */

/* .visually-hidden — the WORST of the twenty. It exists to hide a label from
   sight while keeping it for a screen reader; undefined, the word "Avatar" was
   simply PRINTED in the table header. The clip-rect technique (not display:none,
   not visibility:hidden) is the only one that keeps the text in the
   accessibility tree while removing it visually. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* --- Custom checkbox: the project rule is NO browser defaults. Undefined, this
   rendered as a raw browser checkbox NEXT TO an orphaned SVG tick. --- */
.checkbox {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  cursor: pointer;
  user-select: none;
}

/* The real input stays in the accessibility tree and keeps keyboard focus —
   display:none would remove it from both. It is the :checked/:focus source for
   the visible box beside it. */
.checkbox > input[type="checkbox"],
.perm-check > input[type="checkbox"] {
  position: absolute;
  opacity: 0;
  width: 1.125em;
  height: 1.125em;
  margin: 0;
  cursor: pointer;
}

.checkbox__box,
.perm-check__box {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.125em;
  height: 1.125em;
  flex-shrink: 0;
  border: 1px solid var(--border-medium);
  border-radius: 0.25em;
  background: var(--bg-panel);
  transition: background-color 0.15s ease-out, border-color 0.15s ease-out;
}

.checkbox:hover .checkbox__box,
.perm-check:hover .perm-check__box { border-color: var(--border-dark); }

.checkbox > input:checked + .checkbox__box,
.perm-check > input:checked + .perm-check__box {
  background: var(--primary);
  border-color: var(--primary);
}

/* The tick is drawn at 0 opacity rather than removed, so it fades rather than
   pops, and the box never reflows. */
.checkbox__box > svg,
.perm-check__box > svg {
  width: 0.75em;
  height: 0.75em;
  stroke: var(--primary-fg);
  stroke-width: 2;
  opacity: 0;
  transform: scale(0.6);
  transition: opacity 0.15s ease-out, transform 0.15s ease-out;
}

.checkbox > input:checked + .checkbox__box > svg,
.perm-check > input:checked + .perm-check__box > svg { opacity: 1; transform: scale(1); }

/* Keyboard focus must be visible on the box the user actually sees, since the
   real input is transparent. */
.checkbox > input:focus-visible + .checkbox__box,
.perm-check > input:focus-visible + .perm-check__box {
  outline: 0.125em solid var(--primary);
  outline-offset: 0.125em;
}

.checkbox__label { font-size: var(--text-sm); color: var(--text-primary); }

.perm-check {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

/* A permission granted by a HIGHER permission rather than ticked directly —
   shown as on, but dimmed and not interactive, so an admin can see it is
   implied rather than wonder why unticking does nothing. */
.perm-check.is-implied { opacity: 0.5; cursor: not-allowed; }

/* --- Form layout --- */
.form-grid { display: flex; flex-direction: column; gap: var(--space-md); }
.form-row { display: flex; flex-direction: column; gap: var(--space-xs); }
.form-row__label { font-size: var(--text-sm); font-weight: 500; color: var(--text-primary); }
.form-row__hint { font-size: var(--text-xs); color: var(--text-secondary); }

/* --- Collapsible rule sections (<details>) --- */
.rules {
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
  padding: var(--space-sm) var(--space-md);
  background: var(--bg-page);
}
.rules__summary { cursor: pointer; font-size: var(--text-sm); color: var(--text-primary); }
.rules__body { display: flex; flex-direction: column; gap: var(--space-md); padding-top: var(--space-md); }
.rules__hint { font-size: var(--text-xs); color: var(--text-secondary); margin: 0; }

/* --- Icon-only button. Square by construction: an icon in a text-sized button
   is off-centre, which is what .btn-icon existed to prevent. --- */
.btn-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2em;
  height: 2em;
  padding: 0;
  flex-shrink: 0;
  background: var(--bg-panel);
  border: 1px solid var(--border-default);
  border-radius: 0.375em;
  color: var(--text-secondary);
  cursor: pointer;
  transition: background-color 0.15s ease-out, border-color 0.15s ease-out, color 0.15s ease-out;
}
.btn-icon:hover { background: var(--bg-page); border-color: var(--border-medium); color: var(--text-primary); }
.btn-icon:focus-visible { outline: 0.125em solid var(--primary); outline-offset: 0.125em; }

/* Destructive action. Colour ALONE never carries meaning — the label says what
   it does; this only reinforces it. */
/* --natural-red, not an invented --danger: the token that exists in base.css
   AND in the DB theme, so an admin editing the palette changes this too. */
.btn-danger { border-color: var(--natural-red); color: var(--natural-red); }
.btn-danger:hover { background: var(--natural-red); color: var(--on-strong); border-color: var(--natural-red); }

/* [TARGETED: 2026-07-21] SHARED .empty-state base component. Every consumer
   (public-messages, public-browse, public-home, public-my-services,
   admin-modules, admin-fields, admin-public-site, admin-profiles) already
   markup the SAME nested shape — icon, title, hint, each with its own scoped
   class — but base.css never actually defined the box itself (only
   page-scoped icon-size overrides existed, e.g. .msg-inbox .empty-state__icon
   below). .modq__empty had to be fully self-contained for exactly this
   reason (see its own comment: "base.css has no shared .empty-state layout to
   lean on") — this rule closes that gap ONCE instead of leaving every future
   empty-state to reinvent its own box. Full border + natural color, no
   "fresh nail cut" single-side accent. Mobile and desktop are two explicit,
   separately-declared breakpoints, not one runtime-computed value. */
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-sm);
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
  background: var(--bg-panel);
  color: var(--text-secondary);
  /* Mobile-first default (< 40em): compact padding, matches this app's
     "no wasted space" compact-layout rule for small screens. */
  padding: var(--space-lg) var(--space-md);
}
/* [TARGETED: 2026-07-21] EMPTY-STATE NESTED STRUCTURE. The box above stays a
   flex column; its TWO direct children are now the icon-wrap and the body-wrap
   (was: icon + title + hint as three flat siblings — only 2 levels deep, below
   this project's "nested HTML minimum 3 levels deep, with proper class AND id"
   GUI standard). The nesting is the industry empty-state shape (Shopify Polaris
   EmptyState = image-wrap + a Details box that groups heading+body under ONE
   max-width; Carbon / ServiceNow Horizon = icon slot above a single content
   group), NOT wrappers for their own sake:
     - __icon-wrap is a dedicated slot so the icon can be sized/positioned
       independently of the text (and so admin-modules' JS-injected
       data-icon="module" mark lands inside the SAME wrapper an inline <svg>
       does — hydrateIcons swaps the node in place, wrapper untouched).
     - __body groups title+hint so the max-width constrains them as ONE unit
       (moved off __hint below) instead of the title being unconstrained while
       only the hint wrapped — the two now share one readable measure. */
.empty-state__icon-wrap {
  display: flex;
  align-items: center;
  justify-content: center;
}
.empty-state__body {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xs);
  /* One constraint for title+hint together (was on __hint alone). */
  max-width: 26em;
}
.empty-state__icon {
  width: 2.5em;
  height: 2.5em;
  color: var(--border-medium);
  stroke-width: 1;
  fill: none;
  stroke: currentColor;
}
/* admin-modules seeds its icon slot with the SOLID module mark via
   data-icon="module" (hydrateIcons -> <svg class="icon icon--solid">), NOT an
   inline .empty-state__icon. It must NOT borrow .empty-state__icon, whose
   `fill:none` would blank a filled path (and would win on source order over
   .icon--solid). Size it here through the wrap instead, matching the 2.5em the
   inline outline icons use; fill/stroke stay owned by .icon--solid. */
#modulesEmptyIcon > .icon { width: 2.5em; height: 2.5em; color: var(--border-medium); }
.empty-state__title { font-size: var(--text-md); font-weight: 500; color: var(--text-primary); }
.empty-state__hint { font-size: var(--text-sm); }
/* Desktop (>= 40em): roomier padding — the box has screen space to use, so it
   is no longer fighting for compactness the way the mobile default is. */
@media (min-width: 40em) {
  .empty-state { padding: var(--space-xl) var(--space-lg); }
}

/* --- OAuth row + icon --- */
/* [TARGETED: 2026-07-21] oauth-first layout — #oauthButtons now renders ABOVE
   the sign-in/register form (was below it), so its outer spacing flips from
   margin-top to margin-bottom; a thin divider (.auth-card__divider) sits
   between it and the form, reusing .user-menu__divider's existing 1px-line
   convention rather than a new separator style. */
.auth-card__oauth { display: flex; flex-direction: column; gap: var(--space-sm); margin-bottom: var(--space-md); }
.auth-card__divider { height: 1px; background: var(--border-light, var(--border-default)); margin: var(--space-md) 0; }
/* [TARGETED: 2026-07-21] oauth-btn full-width row — matches Google's own
   "Sign in with Google" button convention (full-width, bordered, centered
   icon+label), which Google/Facebook/Telegram never actually had: this class
   carried zero rules before, so the buttons rendered as unstyled inline <a>
   tags sized only by their text. .auth-card__oauth (a column flex container)
   already stacked them; this is what makes each ROW fill that column. */
.oauth-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  width: 100%;
  background: var(--muted);
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
  color: var(--text-primary);
  padding: 0.5em 1em;
  font-family: inherit;
  font-size: var(--text-base);
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-standard),
    border-color var(--duration-fast) var(--ease-standard),
    transform var(--duration-fast) var(--ease-standard),
    box-shadow var(--duration-fast) var(--ease-standard);
}
.oauth-btn:hover {
  background: var(--accent);
  border-color: var(--border-medium);
  transform: translateY(-2px);
  box-shadow: 0 0.25em 0.75em rgba(0, 0, 0, 0.08);
}
.oauth-btn:active { transform: scale(0.98) translateY(0); box-shadow: 0 0.125em 0.25em rgba(0, 0, 0, 0.1); }
.oauth-btn__icon { display: inline-flex; width: 1.125em; height: 1.125em; flex-shrink: 0; }
/* [TARGETED: 2026-07-21] telegram login custom button — the old
   .oauth-btn-telegram-wrap iframe wrapper rules were REMOVED here. Telegram's
   fixed-width iframe widget was replaced by a custom .oauth-btn--telegram button
   (styled in oauth-buttons.css) that triggers window.Telegram.Login.auth()
   (popup), so there is no longer an iframe to width-match. */

/* --- Custom file input: the native control is unstyleable, so the real input is
   transparent and the label beside it is what the user sees. --- */
.file-input { display: inline-flex; align-items: center; gap: var(--space-sm); }
.file-input > input[type="file"] { flex: 1; min-width: 0; font-size: var(--text-sm); color: var(--text-secondary); }

.skills-picker { display: flex; flex-wrap: wrap; gap: var(--space-sm); }

/* The admin "what the public sees" preview grid. Same shape as the real
   .module-cards it previews — a different grid would make the preview lie. */
.preview-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(11em, 100%), 1fr));
  gap: var(--space-sm);
  margin: var(--space-md) 0 0 0;
  padding: 0;
  list-style: none;
}

/* ===========================================================================
   [TARGETED: 2026-07-17] The PUBLIC MENU — a disclosure, not a popup.
   Every offset below uses LOGICAL properties. That is not stylistic: Bootstrap
   #34088 and #32622 are real, filed bugs where a dropdown positioned with
   right:0/left:auto rendered on the wrong side in RTL. This app serves Arabic
   and Urdu, so a physical `right` here would be that same bug on arrival.
   =========================================================================== */

.public-menu { position: relative; flex-shrink: 0; }

/* [TARGETED: 2026-07-24] HEADER-CONTROLS CLOAK — stop the staggered pop-in.
   The header ships only the SSR sign-in button; the burger menu (.public-menu,
   public-menu.js) and the theme/language cluster (.page-controls,
   page-controls.js + lang-switcher-auto.js) are built by JS after separate
   awaits, so they used to appear one-by-one and shift the row. These two are
   the ONLY JS-injected header children, so we hide just them until hydration
   settles, then reveal BOTH together in one fade — the SSR sign-in button and
   the rest of the page are never hidden, so there is no blank header.

   `body:not(.controls-hydrated)` is the pre-reveal state. public-menu.js /
   lang-switcher-auto.js flip `.controls-hydrated` on <body> once both have
   mounted (end of the language auto-mount, after initI18n resolves — by then the
   menu toggle's synchronous insert has already happened), so the reveal is
   simultaneous, not staggered. opacity only (no layout property) => GPU-friendly
   and, because the elements still occupy their box while transparent, no shift. */
body:not(.controls-hydrated) .public-menu,
body:not(.controls-hydrated) .page-controls {
  opacity: 0;
}
.public-menu,
.page-controls {
  opacity: 1;
  transition: opacity var(--duration-med) var(--ease-standard);
}
@media (prefers-reduced-motion: reduce) {
  .public-menu,
  .page-controls { transition: none; }
}

.public-menu__toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  padding: 0.4em 0.75em;
  background: var(--bg-panel);
  border: 1px solid var(--border-default);
  border-radius: 0.375em;
  color: var(--text-primary);
  font-size: var(--text-sm);
  cursor: pointer;
  transition: background-color 0.15s ease-out, border-color 0.15s ease-out;
}

.public-menu__toggle:hover { background: var(--bg-page); border-color: var(--border-medium); }
.public-menu__toggle:focus-visible { outline: 0.125em solid var(--primary); outline-offset: 0.125em; }

.public-menu__panel {
  position: absolute;
  top: calc(100% + 0.375em);
  /* inset-inline-end, never `right`: mirrors automatically under dir="rtl", and
     the control that FLIPS the page to RTL is inside this very panel. */
  inset-inline-end: 0;
  z-index: 40;   /* above the page-controls cluster (20) and the mobile nav (20) */
  min-width: 15em;
  /* Never wider than the viewport on a narrow phone, which a fixed min-width
     alone would allow — that is how a menu forces the page to scroll sideways. */
  max-width: min(20em, calc(100vw - 2em));
  max-height: min(70vh, 30em);
  overflow-y: auto;
  padding: var(--glass-pad);
  border-radius: var(--glass-radius);

  /* FALLBACK FIRST: a solid opaque panel. A browser without backdrop-filter
     (~5% of traffic) gets this and it is fully legible — never a transparent
     unreadable box. @supports below upgrades the rest. */
  background: var(--bg-panel);
  border: 1px solid var(--border-default);

  /* Layered, context-soft shadow: a tight contact shadow plus a wide soft one,
     the iOS-26 "floating glass" depth. Plus an inset top highlight — the light
     edge that reads as a glass rim catching light. */
  box-shadow:
    0 0.05em 0.15em rgba(0, 0, 0, 0.12),
    0 0.75em 2em rgba(0, 0, 0, 0.18),
    inset 0 0.05em 0 var(--on-strong-dim);

  /* The open motion: scale + fade from the top-end corner (where the trigger is),
     with the spring-like ease. transform-origin is logical-ish via the corner. */
  transform-origin: top var(--menu-origin, right);
  animation: glassOpen 0.28s var(--glass-ease);
  box-shadow: 0 0.25em 0.75em rgba(0, 0, 0, 0.12);
  animation: fadeIn 0.15s ease-out;
}

/* [hidden] must WIN. Without this the panel's display:block would beat the
   attribute and the menu would never close — and `hidden` is also what keeps the
   links out of the tab order, so a "closed" menu whose items are still focusable
   is a real WCAG 2.4.3 failure, not a cosmetic one. */
.public-menu__panel[hidden] { display: none; }

.public-menu__section { padding: var(--space-xs) 0; }
.public-menu__section + .public-menu__section {
  /* border-block-start, not border-top: correct under both writing modes. */
  border-block-start: 1px solid var(--border-light);
  margin-block-start: var(--space-xs);
}

.public-menu__list { list-style: none; margin: 0; padding: 0; }

.public-menu__heading {
  display: block;
  padding: var(--space-xs) var(--space-sm);
  font-size: var(--text-xs);
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.public-menu__item {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm);
  border-radius: 0.25em;
  color: var(--text-primary);
  text-decoration: none;
  font-size: var(--text-sm);
  transition: background-color 0.15s ease-out;
}

.public-menu__item:hover { background: var(--bg-page); }
.public-menu__item:focus-visible { outline: 0.125em solid var(--primary); outline-offset: -0.125em; }

/* The current section. Weight AND background, never colour alone — colour alone
   is invisible to anyone who cannot distinguish it. */
.public-menu__item.is-active { background: var(--bg-page); font-weight: 600; }

.public-menu__label { flex: 1; min-width: 0; overflow-wrap: anywhere; }

/* [TARGETED: 2026-07-20] Signed-in member's real photo, replacing the generic
   user glyph — same circular treatment as the /@username profile card
   (.profile-card__picture), scaled down to sit inline with the menu icons
   beside it (.icon is 1.25em, so this sits comfortably one step up). */
.public-menu__avatar {
  width: 1.5em;
  height: 1.5em;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
  border: 1px solid var(--border-default);
}

.public-menu__count {
  /* margin-inline-start: pushes AWAY from the label in both directions. */
  margin-inline-start: auto;
  flex-shrink: 0;
}

.public-menu__item--accent { color: var(--primary); font-weight: 600; }

/* The toggle's word is redundant beside the icon on a narrow screen, where the
   header has no room for it. The icon plus the button's accessible name carry it. */
@media (max-width: 30em) {
  .public-menu__toggle-label { display: none; }
}

/* ===========================================================================
   [TARGETED: 2026-07-20] UI-0001 — CONSOLIDATED SINGLE PANEL (.account-panel).
   Replaces the old three-widgets-side-by-side header (hamburger + separate
   avatar dropdown + separate lang/theme/reset cluster) with ONE trigger and
   ONE panel. `.account-panel` IS `.public-menu` (public-menu.js sets both
   classes on the same wrap element) — these rules layer on top of the
   .public-menu__* rules above rather than duplicating them; only what is
   NEW or DIFFERENT for the consolidated shape is defined here.

   Device rendering (spec section 4/3c): single responsive markup, CSS-driven
   breakpoints — no server-side device-class branching, matching this file's
   existing convention (39 pre-existing media queries, zero server HTML
   branches) and the 2026 guidance this ticket's research confirmed.
   =========================================================================== */

/* `.public-menu__panel` (`panel`) carries the native `popover` attribute here
   (same element the pre-existing .public-menu__panel rules above already
   style for the non-popover fallback path — [hidden]) — matching
   `.user-menu__panel`'s own `[popover]` shape exactly: trigger and panel are
   SIBLINGS inside a plain, non-popover `.account-panel`/`.public-menu`
   wrapper, never trigger-inside-popover-content (that structure was caught
   and fixed during implementation — see the comment in public-menu.js). A
   `[popover]` element renders in the top layer with UA defaults (border,
   inset, margin, width) that must be reset before this app's own
   .public-menu__panel chrome (already defined above) applies.

   SAME containing-block quirk as .user-menu__panel: position:absolute here
   is only the CLOSED-state default (matters before JS runs, i.e. never
   visibly, since [popover] keeps it invisible until then) — public-menu.js's
   positionAccountPanel() sets position:fixed + explicit top/left-or-right via
   getBoundingClientRect() every time the panel opens, RTL-aware. */
.public-menu__panel[popover] {
  inset: auto;
  margin: 0;
  /* .public-menu__panel above already sets position/inset-inline-end/
     top/width/padding/background/border/shadow/animation — this rule only
     clears the UA popover defaults that would otherwise fight those. */
}
.public-menu__panel:popover-open { display: block; }

/* Trigger avatar (spec 4a/4d): shown INSTEAD OF the generic menu icon once
   signed in, so "easy access to your own profile" is true at the trigger
   level, before the panel even opens. Same circular treatment as
   .public-menu__avatar / .user-menu__avatar — one shared visual language for
   "this is a photo of you" across every surface in the app. */
.account-panel__toggle-avatar {
  width: 1.5em;
  height: 1.5em;
  border-radius: 50%;
  overflow: hidden;
  flex-shrink: 0;
  border: 1px solid var(--border-default);
  display: inline-flex;
}
.account-panel__toggle-avatar > img { width: 100%; height: 100%; object-fit: cover; }

/* Identity header — the panel's leading row (Baymard / shadcn "Profile
   Dropdown" convention researched for this ticket: avatar+name header,
   divider, primary actions, divider, sign-out at the bottom). Signed-in
   content here comes from public-user-menu.js's renderPublicUserIdentity();
   signed-out content is this file's own brand-mark + Sign-in row. Either
   way, this section sits ABOVE `.account-panel__sections` with its own
   bottom divider, matching `.public-menu__section + .public-menu__section`'s
   existing divider treatment. */
.account-panel__identity {
  padding: var(--space-sm) var(--space-sm) var(--space-md);
  border-block-end: 1px solid var(--border-light);
  margin-block-end: var(--space-xs);
}
.account-panel__identity:empty { display: none; }

.account-panel__identity--signed-out {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

/* Brand mark (spec 4d) — the SAME svg already shipped on the login/register
   pages (.auth-card__logo), reused rather than a new asset. Sized down to sit
   beside a single action row instead of leading a full auth-card header;
   Rule 5: explicit em sizing via this scoped rule, never a bare <svg>. */
.account-panel__brand-mark {
  width: 2em;
  height: 1.79em;   /* preserves the source viewBox's 512:456.96 aspect ratio */
  flex-shrink: 0;
}

/* Preferences section (language + theme + reset, relocated here from the old
   floating #pageControls cluster by absorbPageControls() in public-menu.js).

   [TARGETED: 2026-07-20] CORRECTED DURING LIVE TESTING: an earlier draft here
   had a `.account-panel__prefs-host > .page-controls { ... }` rule, on the
   mistaken assumption that the `.page-controls` DIV ITSELF gets relocated.
   It does not — absorbPageControls() moves only `.page-controls`'s CHILDREN
   (.lang-switcher, .theme-toggle, #resetStorage) into this host, then
   discards the now-empty `.page-controls` shell entirely
   (`cluster.remove()`). So a `.account-panel__prefs-host > .page-controls`
   selector can never match anything — dead CSS, removed rather than left in.
   The real fix for "the relocated lang-switcher lost its dropdown styling"
   lives where that CSS actually was: the `.page-controls > .lang-switcher >
   ...` rule block above now also lists `.account-panel__prefs-host > …` as a
   second selector on every affected rule. */
.account-panel__prefs-host {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-xs);
  padding-block-start: var(--space-xs);
}

/* Children arrive as bare `.lang-switcher` / `.theme-toggle` / `#resetStorage`
   elements (former direct children of `.page-controls`, which drew all the
   shared chrome via its OWN base rule — `border`, `background`, the
   `:first-child`/`:last-child` radius, the `* + *` hairline join). None of
   that cluster-level chrome exists here any more (there is no `.page-controls`
   wrapper left), so this section draws its own equivalent: a column stack
   with a hairline between rows, matching `.public-menu__section +
   .public-menu__section`'s divider treatment used everywhere else in this
   panel.

   .lang-switcher stretches full-width (its trigger already reads as a row —
   icon + native-language-name label, per lang-switcher.js — so full-width
   matches every other row in the panel). .theme-toggle and #resetStorage stay
   their own natural icon-only square size (NOT stretched to 100% — both
   .theme-toggle's and reset-storage.js's own base CSS/markup are icon-only
   with no visible label text, and neither file's DOM is touched by this
   ticket, so stretching them to a full-width row would center a lone icon in
   empty space rather than reading as a real row). Left-aligned, small, and
   compact instead — consistent with "no wasted space". */
.account-panel__prefs-host > .lang-switcher { width: 100%; }
.account-panel__prefs-host > * + * {
  border-block-start: 1px solid var(--border-light);
}
.account-panel__prefs-host > .theme-toggle,
.account-panel__prefs-host > #resetStorage {
  align-self: flex-start;
  margin-block: var(--space-xs);
  margin-inline-start: var(--space-sm);
}

/* Accordion collapse for the Language row (spec 4c / user's "expand a little,
   collapse a little" request). Purely visual/structural — the underlying
   .lang-switcher / .lang-switcher__menu open-close (is-open class, toggled by
   lang-switcher.js, unmodified) still works exactly as it always has; this
   only adds a chevron affordance and lets the accordion state (aria-expanded,
   set by public-menu.js) drive a rotate, matching .user-menu__chevron's
   existing rotate-on-open convention rather than inventing a new one. */
.account-panel__lang--collapsible > .lang-switcher__trigger {
  position: relative;
  width: 100%;
  justify-content: space-between;
}
.account-panel__lang--collapsible > .lang-switcher__trigger::after {
  content: '';
  width: 0.5em;
  height: 0.5em;
  margin-inline-start: auto;
  border-inline-end: 1px solid currentColor;
  border-block-end: 1px solid currentColor;
  transform: rotate(45deg);
  transition: transform 0.15s ease-out;
  flex-shrink: 0;
  opacity: 0.6;
}
.account-panel__lang--collapsible > .lang-switcher__trigger[aria-expanded="true"]::after {
  transform: rotate(-135deg);
}

/* Per-device-class rendering (spec section 4/3c): CSS breakpoints, not a
   server HTML branch. Phone: full-bleed-ish width capped to the viewport
   (inherits .public-menu__panel's existing max-width: min(20em, 100vw - 2em)
   rule — untouched). Tablet/laptop and desktop: the panel's own min-width
   (15em, from .public-menu__panel above) is already generous enough that no
   additional breakpoint is needed for those two tiers to render correctly —
   confirmed by the existing @media (max-width: 30em) rule above being the
   ONLY width-gated behaviour this component ever needed. */

/* ===========================================================================
   [TARGETED: 2026-07-17] MESSAGES — mobile and desktop are genuinely different
   layouts, not one layout squeezed.

   DESKTOP: two panes side by side. You see the inbox AND the thread.
   PHONE:   two SCREENS. The inbox, then the thread, with a back button.
   One template, one breakpoint. Two templates would drift.
   =========================================================================== */

/* svh on the shell, NOT dvh or vh.
   - vh  on iOS is the LARGEST viewport (URL bar hidden), so a 100vh shell is
     taller than the screen and the compose bar sits below the fold on load.
   - dvh recalculates on every toolbar show/hide, which reflows the whole page
     as you scroll — jank on exactly the surface people scroll most.
   - svh is the SMALLEST viewport: the shell always fits, and never resizes. */
.msg-page {
  display: flex;
  flex-direction: column;
  height: 100svh;
  /* Fallback first for any engine without svh; the modern value wins below. */
  height: 100vh;
  height: 100svh;
  overflow: hidden;   /* the LIST scrolls, never the page */
}

.msg-main {
  flex: 1;
  min-height: 0;      /* the flex-child rule that lets an inner list scroll */
  display: grid;
  /* Desktop: a fixed inbox column, the thread taking the rest. */
  grid-template-columns: minmax(16em, 22em) 1fr;
  overflow: hidden;
}

/* --- INBOX --- */
.msg-inbox {
  display: flex;
  flex-direction: column;
  min-height: 0;
  border-inline-end: 1px solid var(--border-default);  /* logical: mirrors in RTL */
  overflow: hidden;
}

.msg-inbox__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: var(--space-md) var(--space-lg);
  border-block-end: 1px solid var(--border-light);
}

.msg-inbox__head > h1 { font-size: var(--text-lg); margin: 0; }

/* [TARGETED: 2026-07-19] INBOX SEARCH — Apple "search field in a navigation bar".
   DEFAULT (mobile / pointer:coarse): collapsed to 0 height, revealed by the pull
   gesture (JS toggles .is-revealed) or followed by the finger via the --pull
   custom prop. DESKTOP (hover + pointer:fine): always pinned open. Full borders,
   tokens, em units, sized SVG. */
.msg-search {
  display: block;
  overflow: hidden;
  /* collapsed by default; --pull follows the finger, .is-revealed opens fully */
  height: var(--pull, 0);
  max-height: 3.2em;
  transition: height var(--duration-med) var(--ease-standard);
  padding-inline: var(--space-md);
}
.msg-search.is-revealed { height: 3.2em; }
.msg-search.is-pinned,               /* JS adds this on non-coarse pointers */
.msg-search { --pull: 0; }
.msg-search__field {
  display: flex; align-items: center; gap: var(--space-sm);
  margin: var(--space-sm) 0;
  padding: 0.35em 0.7em;
  border: 0.0625em solid var(--border-default);   /* FULL border, all sides */
  border-radius: 0.6em;
  background: var(--bg-page);
  transition: border-color var(--duration-fast) var(--ease-standard);
}
.msg-search__field:focus-within { border-color: var(--primary); }
.msg-search__icon { flex-shrink: 0; width: 1.05em; height: 1.05em; color: var(--text-secondary); }
.msg-search__input {
  flex: 1; min-width: 0; border: none; background: none; outline: none;
  font: inherit; font-size: var(--text-sm); color: var(--text-primary);
}
.msg-search__input::placeholder { color: var(--text-secondary); }

/* DESKTOP: search is always visible + pinned; the gesture never applies. */
@media (hover: hover) and (pointer: fine) {
  .msg-search { height: auto; }
}
@media (prefers-reduced-motion: reduce) {
  .msg-search { transition: none; }
  .msg-search__field { transition: none; }
}

.msg-inbox__list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* Stops a phone rubber-band from scrolling the page behind the list — MDN's
     own worked example for this property is literally a chat window. */
  overscroll-behavior-y: contain;
  list-style: none;
  margin: 0;
  padding: var(--space-xs);
}

/* [TARGETED: 2026-07-19] Conversation ROW — HORIZONTAL: the listing thumbnail
   leads (inline-START, so LEFT in English/LTR and mirrored to the RIGHT in
   Arabic/Urdu — automatic via flex + logical properties, no dir-specific CSS),
   the text column follows. All spacing on `gap`; every value a token or em. */
.msg-inbox__item {
  display: flex;
  flex-direction: row;          /* image | details — NOT stacked */
  align-items: center;
  gap: var(--space-sm);
  width: 100%;
  padding: var(--space-sm) var(--space-md);
  background: none;
  /* A transparent FULL border at rest reserves the space, so the active state's
     visible border (all four sides — no nail-cut) adds no layout shift. */
  border: 0.0625em solid transparent;
  border-radius: var(--radius);
  text-align: start;            /* logical: reverses in RTL, unlike text-align:left */
  cursor: pointer;
  color: var(--text-primary);
  transition: background-color var(--duration-fast) var(--ease-standard),
              border-color var(--duration-fast) var(--ease-standard);
}

/* Distinct states — hover, keyboard focus, open, and unread are each visually
   separable (not the same background twice), and unread never relies on colour
   alone (WCAG 1.4.1): it also bolds the name + shows the count badge. */
.msg-inbox__item:hover { background: var(--muted); }
.msg-inbox__item:focus-visible { outline: 0.125em solid var(--primary); outline-offset: -0.125em; }
.msg-inbox__item.is-active {
  /* [TARGETED: 2026-07-19] FULL border on all four sides (natural accent-tinted
     colour) + a subtle background tint — NOT a left-only "fresh nail cut" accent
     bar (forbidden). The border is inset via a transparent 1px border on the
     resting state (below) so the row does not shift when it becomes active. */
  background: color-mix(in oklab, var(--primary) 10%, transparent);
  border-color: color-mix(in oklab, var(--primary) 45%, var(--border-default));
}

/* The text column. min-width:0 is the load-bearing flexbox truncation fix —
   without it a long name/preview forces the row to overflow instead of clipping
   (the classic flex ellipsis gotcha). */
.msg-inbox__col {
  display: flex;
  flex-direction: column;
  gap: 0.1em;
  min-width: 0;
  flex: 1;
}

/* The listing thumbnail sits at the inline-start (left in LTR). It never shrinks
   and never grows — a fixed, crisp square. Detailed sizing lives in .msg-thumb. */
.msg-inbox__thumb { flex-shrink: 0; }

/* Heading row: NAME (truncates) at the inline-start, TIME at the inline-end. */
.msg-inbox__top {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-sm); min-width: 0;   /* let the name truncate inside the row */
}
/* Name: ONE line, ellipsis — a clean scannable row. */
.msg-inbox__name {
  font-size: var(--text-sm); font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.msg-inbox__item.is-unread .msg-inbox__name { font-weight: 700; }
/* Timestamp: never truncates (flex-shrink:0), sits opposite the name. Uses
   tabular-nums so "2:30"/"12:05" columns line up cleanly. */
.msg-inbox__time {
  flex-shrink: 0; font-size: var(--text-xs); color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}
.msg-inbox__item.is-unread .msg-inbox__time { color: var(--primary); font-weight: 600; }

/* Bottom row: last-message PREVIEW (truncates) + the unread BADGE pill. */
.msg-inbox__bottom {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-sm); min-width: 0;
}

/* Listing title + last-message preview: each ONE line, clipped, so the row
   height is constant no matter the content length. */
.msg-inbox__record,
.msg-inbox__preview {
  font-size: var(--text-xs); color: var(--text-secondary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.msg-inbox__item.is-unread .msg-inbox__preview { color: var(--text-primary); }

/* Unread BADGE — a proper pill (full radius, full contrast). Three unread
   signals total (bold name + row tint + this numeric pill) so it never relies on
   colour alone (WCAG 1.4.1). tabular-nums keeps 1 vs 12 vs 99+ balanced. */
.msg-inbox__badge {
  flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.25em; height: 1.25em; padding: 0 0.375em;
  border-radius: 999em;
  background: var(--primary); color: var(--primary-fg);
  font-size: var(--text-xs); font-weight: 600; line-height: 1;
  font-variant-numeric: tabular-nums;
}
/* Subtle full-row tint for an unread conversation — the second (colour-independent
   when paired with weight) signal. */
.msg-inbox__item.is-unread { background: color-mix(in oklab, var(--primary) 6%, transparent); }

/* [TARGETED: 2026-07-19] FIND-IN-CONVERSATION bar (below the thread header). */
.msg-find {
  display: block; padding: var(--space-sm) var(--space-lg);
  border-block-end: 0.0625em solid var(--border-light);
  background: var(--bg-panel);
}
.msg-find__field {
  display: flex; align-items: center; gap: var(--space-xs);
  border: 0.0625em solid var(--border-default); border-radius: 0.6em;
  padding: 0.2em 0.4em; background: var(--bg-page);
  transition: border-color var(--duration-fast) var(--ease-standard);
}
.msg-find__field:focus-within { border-color: var(--primary); }
.msg-find__input {
  flex: 1; min-width: 0; border: none; background: none; outline: none;
  font: inherit; font-size: var(--text-sm); color: var(--text-primary);
}
.msg-find__count { flex-shrink: 0; font-size: var(--text-xs); color: var(--text-secondary); font-variant-numeric: tabular-nums; }
.msg-find__nav, .msg-find__close { flex-shrink: 0; }
.msg-find__nav .icon, .msg-find__close .icon { width: 1em; height: 1em; }
/* Match highlight in the thread. --match-current is the focused one. */
.msg-bubble--match { outline: 0.125em solid color-mix(in oklab, var(--primary) 50%, transparent); outline-offset: 0.1em; }
.msg-bubble--match-current { outline-color: var(--primary); background: color-mix(in oklab, var(--primary) 12%, var(--bg-panel)); }

/* [TARGETED: 2026-07-19] LISTINGS SEARCH on the browse page (always visible). */
.browse-search { display: block; max-width: 32em; margin: 0 auto var(--space-lg); }
.browse-search__field {
  display: flex; align-items: center; gap: var(--space-sm);
  border: 0.0625em solid var(--border-default); border-radius: 0.6em;
  padding: 0.45em 0.8em; background: var(--bg-panel);
  transition: border-color var(--duration-fast) var(--ease-standard);
}
.browse-search__field:focus-within { border-color: var(--primary); }
.browse-search__icon { flex-shrink: 0; width: 1.1em; height: 1.1em; color: var(--text-secondary); }
.browse-search__input {
  flex: 1; min-width: 0; border: none; background: none; outline: none;
  font: inherit; font-size: var(--text-sm); color: var(--text-primary);
}
@media (prefers-reduced-motion: reduce) {
  .msg-find__field, .browse-search__field { transition: none; }
}

/* --- THREAD --- */
.msg-thread {
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
  position: relative;   /* [TARGETED: 2026-07-19] anchor for the jump-to-latest pill */
}

.msg-thread__head {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-md) var(--space-lg);
  border-block-end: 1px solid var(--border-light);
}

/* The back button is a PHONE affordance: on desktop both panes are visible, so
   "back" would mean nothing. */
.msg-thread__back { display: none; }

.msg-thread__who { display: flex; flex-direction: column; min-width: 0; }
.msg-thread__record { font-size: var(--text-xs); color: var(--text-secondary); }

/* Trailing safety controls (block / report). margin-inline-start:auto pushes
   them to the far edge — logical, so it mirrors under RTL. */
.msg-thread__actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-inline-start: auto;
}
.msg-thread__action {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: 0.35em 0.6em;
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
  background: var(--bg-panel);
  color: var(--text-secondary);
  font-size: var(--text-xs);
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.msg-thread__action:hover {
  background: var(--bg-page);
  border-color: var(--border-medium);
  color: var(--text-primary);
}
.msg-thread__action:focus-visible {
  outline: 0.125em solid var(--primary);
  outline-offset: 0.0625em;
}
.msg-thread__action-label { white-space: nowrap; }

/* On the phone layout the label is dropped to save the head row; the icon and
   its aria-label carry the meaning. */
@media (max-width: 40em) {
  .msg-thread__action-label { display: none; }
}

.msg-thread__list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior-y: contain;
  /* THE STICK-TO-BOTTOM. overflow-anchor is the browser's own scroll anchoring:
     as messages append, the view stays pinned to what you were reading instead
     of jumping. `auto` is the default — it is written explicitly because it is
     load-bearing here and must not be "tidied away" by someone later. */
  overflow-anchor: auto;
  list-style: none;
  margin: 0;
  padding: var(--space-lg);
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.msg-row { display: flex; }
/* flex-end, not float/margin-left: reverses correctly under RTL for free. */
.msg-row--mine { justify-content: flex-end; }

.msg-bubble {
  max-width: min(32em, 78%);
  padding: var(--space-sm) var(--space-md);
  background: var(--bg-panel);
  border: 1px solid var(--border-default);
  /* Asymmetric corner: the "tail" side is square. Logical corners so the tail
     is on the correct side in RTL without a second rule. */
  border-radius: var(--radius);
  border-end-start-radius: 0.15em;
}

.msg-row--mine > .msg-bubble {
  background: var(--primary);
  color: var(--primary-fg);
  border-color: var(--primary);
  border-end-start-radius: var(--radius);
  border-end-end-radius: 0.15em;
}

.msg-bubble__text { margin: 0; font-size: var(--text-sm); overflow-wrap: anywhere; white-space: pre-wrap; }
.msg-bubble__image { display: block; margin-bottom: var(--space-xs); max-width: 14em; aspect-ratio: auto; }

.msg-bubble__meta {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.35em;
  margin-block-start: 0.2em;
  font-size: var(--text-xs);
  opacity: 0.75;
}

.msg-receipt { display: inline-flex; width: 1em; height: 1em; }
.msg-receipt > svg { width: 100%; height: 100%; }
/* The read tick. Colour is a REINFORCEMENT: the accessible name on the element
   says "Read (thread_open)", so a colour-blind user is not relying on the hue. */
/* --primary, not an invented --natural-blue: it exists in base.css AND in the
   DB theme, so an admin editing the palette changes this tick too. On my own
   bubble the background IS --primary, so the tick uses --primary-fg there —
   otherwise it would be invisible against itself. */
.msg-receipt--read { opacity: 1; }
.msg-row:not(.msg-row--mine) .msg-receipt--read { color: var(--primary); }

/* ======================================================================
   [TARGETED: 2026-07-19] MESSAGING UX OVERHAUL — status tiers, dividers,
   grouping, jump pill, toast, connection banner, blocked compose, char
   counter, and motion (all with prefers-reduced-motion guards).
   Tokens only; em units; natural colours; full borders; RTL-safe logicals.
   ====================================================================== */

/* --- status tiers on my own bubble's tick --- */
.msg-receipt--sending { opacity: 0.6; }
.msg-receipt--sending > svg { animation: msg-spin 1.4s linear infinite; }
.msg-receipt--sent    { opacity: 0.75; }
.msg-receipt--delivered { opacity: 0.9; }
.msg-receipt--failed  { color: var(--natural-red, #ef4444); opacity: 1; }
/* delivered -> read is a colour+opacity transition, not a pop */
.msg-receipt { transition: color 0.2s ease-out, opacity 0.2s ease-out; }

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

/* --- same-sender grouping: collapse the gap between grouped bubbles --- */
.msg-row { margin-block-start: var(--space-sm); }
.msg-row--grouped { margin-block-start: 0.15em; }

/* --- bubble entrance + hover-reveal exact time --- */
.msg-row { animation: msg-in 0.2s ease-out both; }
@keyframes msg-in { from { opacity: 0; transform: translateY(0.4em); } to { opacity: 1; transform: none; } }
.msg-bubble__meta time { cursor: default; }

/* --- day divider --- */
.msg-day {
  display: flex; align-items: center; justify-content: center;
  margin: var(--space-md) 0; list-style: none;
}
.msg-day__label {
  font-size: var(--text-xs); color: var(--text-secondary);
  background: var(--bg-panel); border: 1px solid var(--border-default);
  border-radius: 1em; padding: 0.15em 0.8em;
}

/* --- unread separator line --- */
.msg-unread-sep {
  display: flex; align-items: center; gap: var(--space-sm);
  margin: var(--space-md) 0; list-style: none;
  color: var(--primary);
}
.msg-unread-sep::before, .msg-unread-sep::after {
  content: ''; flex: 1; height: 0.0625em; background: var(--primary); opacity: 0.4;
}
.msg-unread-sep__label { font-size: var(--text-xs); font-weight: 600; }

/* --- jump-to-latest pill --- */
.msg-jump {
  position: absolute; inset-inline-end: var(--space-lg); bottom: 5em;
  display: inline-flex; align-items: center; gap: 0.3em;
  padding: 0.4em 0.9em; font-size: var(--text-xs); font-weight: 600;
  background: var(--primary); color: var(--primary-fg);
  border: none; border-radius: 1.4em;
  box-shadow: var(--shadow-md); cursor: pointer;
  transition: transform 0.15s ease-out, box-shadow 0.15s ease-out;
}
.msg-jump:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg); }
.msg-jump .icon { width: 1em; height: 1em; }

/* --- toast --- */
.msg-toast {
  position: fixed; inset-inline: 0; bottom: 1.5em; margin-inline: auto;
  width: max-content; max-width: 90vw;
  padding: 0.6em 1.1em; border-radius: 0.5em;
  background: var(--text-primary); color: var(--bg-page);
  font-size: var(--text-sm); box-shadow: var(--shadow-lg);
  opacity: 0; transform: translateY(0.6em); pointer-events: none;
  transition: opacity 0.2s ease-out, transform 0.2s ease-out;
  z-index: 50;
}
.msg-toast.is-visible { opacity: 1; transform: none; }
/* On-error toast: the reddish surface + its readable text, both from the paired
   status tokens (bg/fg), so it stays legible in light AND dark — no literal #fff. */
.msg-toast.is-error { background: var(--status-bad-fg); color: var(--status-bad-bg); }

/* --- connection banner --- */
.msg-conn {
  text-align: center; font-size: var(--text-xs);
  padding: 0.3em; color: var(--primary-fg);
}
/* Paired status tokens carry both the surface and a contrast-checked text colour
   for each state — no hardcoded #3a2a00 / #06310f that only worked in one theme. */
.msg-conn.is-reconnecting { background: var(--status-warn-bg); color: var(--status-warn-fg); }
.msg-conn.is-connected { background: var(--status-ok-bg); color: var(--status-ok-fg); }

/* --- blocked compose note + disabled state --- */
.msg-compose-wrap { display: flex; flex-direction: column; }
.msg-compose__blocked {
  margin: 0; padding: var(--space-sm) var(--space-lg);
  font-size: var(--text-xs); color: var(--text-secondary);
  background: var(--muted); border-block-start: 1px solid var(--border-default);
}
.msg-compose.is-blocked { opacity: 0.6; }
.msg-compose__input:disabled { cursor: not-allowed; }

/* --- char counter --- */
.msg-compose { position: relative; }
.msg-compose__count {
  position: absolute; inset-inline-end: 3.4em; bottom: 0.4em;
  font-size: var(--text-xs); color: var(--text-secondary); pointer-events: none;
}
.msg-compose__count.is-low { color: var(--natural-red, #ef4444); font-weight: 600; }

/* [TARGETED: 2026-07-19] LISTING THUMBNAIL — the service a conversation is about,
   on each inbox row and in the thread header. Rounded square (distinct from a
   circular person-avatar), object-fit cover so no photo distorts, theme-aware
   loading background (never a white flash in dark mode), isolation:isolate to
   dodge the Safari border-radius+overflow clip bug. Fixed em box → no CLS. */
.msg-thumb {
  flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.75em; height: 2.75em;
  border-radius: 0.5em; overflow: hidden; isolation: isolate;
  border: 1px solid var(--border-default);
  background: var(--muted);           /* loading/letterbox colour, follows theme */
  color: var(--text-secondary);
}
.msg-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Empty / no-image / failed: a calm glyph tile, never a broken-image icon. */
.msg-thumb--empty > svg { width: 60%; height: 60%; opacity: 0.55; }

/* (Inbox row layout — .msg-inbox__item / __col / __thumb — is defined once, in
   the consolidated conversation-ROW block above. This former duplicate was
   removed 2026-07-19 so the row's flex-direction:row + truncation rules aren't
   silently overridden by a second, less complete copy.) */

/* Thread header: the listing card (thumbnail + name + title), a single link. */
.msg-thread__listing {
  display: flex; align-items: center; gap: var(--space-sm);
  min-width: 0; text-decoration: none; color: inherit; border-radius: 0.5em;
  padding: 0.15em 0.3em; transition: background 0.15s ease-out;
}
.msg-thread__listing:hover { background: var(--muted); }
.msg-thread__listing:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.msg-thread__thumb { width: 2.5em; height: 2.5em; }
.msg-thread__who { display: flex; flex-direction: column; min-width: 0; }
.msg-thread__record { font-size: var(--text-xs); color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

@media (prefers-reduced-motion: reduce) { .msg-thread__listing { transition: none; } }

/* [TARGETED: 2026-07-19] Inline "conversation unavailable" deep-link state —
   centred in the thread pane, calm and non-alarming (a bad link is not an error
   the user caused). Tokens, em units, sized SVG. */
.msg-unavailable {
  flex: 1; display: flex; flex-direction: column;
  align-items: center; justify-content: center; text-align: center;
  gap: var(--space-sm); padding: var(--space-lg); color: var(--text-secondary);
  max-width: 24em; margin-inline: auto;
}
.msg-unavailable__icon { width: 2.5em; height: 2.5em; opacity: 0.6; }
.msg-unavailable__text { font-size: var(--text-sm); line-height: 1.5; }

/* --- "load earlier messages" sentinel (keyset pagination, scroll-up) --- */
.msg-earlier {
  display: flex; align-items: center; justify-content: center;
  padding: var(--space-sm) 0; list-style: none; min-height: 2.2em;
}
.msg-earlier__btn {
  display: inline-flex; align-items: center; gap: 0.4em;
  padding: 0.35em 0.9em; font-size: var(--text-xs); font-weight: 500;
  color: var(--text-secondary);
  background: var(--bg-panel); border: 1px solid var(--border-default);
  border-radius: 1.2em; cursor: pointer;
  transition: background 0.15s ease-out, border-color 0.15s ease-out, color 0.15s ease-out;
}
.msg-earlier__btn:hover { background: var(--muted); color: var(--text-primary); border-color: var(--border-strong, var(--text-secondary)); }
.msg-earlier__btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.msg-earlier__btn .icon { width: 1em; height: 1em; }
.msg-earlier__spin { display: inline-flex; color: var(--text-secondary); }
.msg-earlier__spin .icon { width: 1.2em; height: 1.2em; animation: msg-spin 1.4s linear infinite; }

/* --- inbox loading skeleton (NN/g: skeleton for list load) --- */
.msg-inbox__skeleton { list-style: none; margin: 0; padding: 0; }
.msg-skel-row {
  display: flex; flex-direction: column; gap: var(--space-xs);
  padding: var(--space-md); border-block-end: 1px solid var(--border-default);
}
.msg-skel {
  display: block; height: 0.8em; border-radius: 0.3em;
  background: linear-gradient(90deg, var(--muted) 25%, var(--border-default) 37%, var(--muted) 63%);
  background-size: 400% 100%;
  animation: msg-shimmer 1.4s ease-in-out infinite;
}
.msg-skel--name { width: 45%; height: 0.9em; }
.msg-skel--line { width: 80%; }
@keyframes msg-shimmer { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }

/* --- MOTION: honour prefers-reduced-motion (WCAG 2.3.3) --- */
@media (prefers-reduced-motion: reduce) {
  .msg-row { animation: none; }
  .msg-receipt--sending > svg { animation: none; }
  .msg-skel { animation: none; }
  .msg-earlier__spin .icon { animation: none; }
  .msg-receipt, .msg-jump, .msg-toast, .msg-earlier__btn, .msg-inbox__item { transition: none; }
}

/* [TARGETED: 2026-07-19] The thread pane's "choose/welcome" placeholder was
   REMOVED entirely (user request): once a member has conversations, prompting
   them to "choose a conversation" is pointless. When no thread is open the pane
   is simply empty; they pick from the inbox list. The "no conversations yet"
   hint for a brand-new user lives in the INBOX pane (.msg-inbox .empty-state). */

/* --- COMPOSE --- */
.msg-compose {
  display: flex;
  align-items: flex-end;
  gap: var(--space-sm);
  padding: var(--space-md) var(--space-lg);
  /* safe-area-inset-bottom: on a notched phone the home indicator sits over the
     bottom of the viewport. Without this the send button is under it. max() so
     the normal padding still applies on a device with no inset. */
  padding-bottom: max(var(--space-md), env(safe-area-inset-bottom));
  border-block-start: 1px solid var(--border-default);
  background: var(--bg-panel);
}

.msg-compose__input {
  flex: 1;
  min-width: 0;
  resize: none;
  max-height: 10em;
  /* 16px minimum. Below 16px, iOS Safari ZOOMS the page when the field is
     focused and does not zoom back — a documented, infuriating behaviour.
     --text-base is 0.875rem = 14px, so this deliberately does NOT use it. */
  font-size: 1rem;
  line-height: 1.4;
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--border-default);
  border-radius: 1.2em;
  background: var(--bg-page);
  color: var(--text-primary);
}

.msg-compose__input:focus-visible { outline: none; border-color: var(--primary); }
/* [TARGETED: 2026-07-19] Send button — a circular icon-button carrying the
   user-supplied filled arrow glyph. DESKTOP: compact 2.4em circle. MOBILE
   (touch): grows to 2.75em (~44px, the accessible minimum tap target). The
   button is square/round (no text), so it overrides .btn's default padding. */
.msg-compose__send {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.4em;
  height: 2.4em;
  padding: 0;
  border-radius: 50%;
  /* nudge the arrow glyph to sit optically centred in the circle */
  transition: transform 0.15s ease-out, filter 0.15s ease-out, background 0.15s ease-out;
}
.msg-compose__send-icon {
  width: 1.35em;
  height: 1.35em;
  display: block;
}
/* Press + hover feedback (subtle, per the GUI standard). */
.msg-compose__send:hover:not(:disabled) { filter: brightness(1.06); transform: translateY(-1px); }
.msg-compose__send:active:not(:disabled) { transform: scale(0.94); }
.msg-compose__send:disabled { opacity: 0.5; cursor: not-allowed; }

/* Mobile / touch: a bigger, thumb-friendly target. */
@media (hover: none), (max-width: 44em) {
  .msg-compose__send { width: 2.75em; height: 2.75em; }
  .msg-compose__send-icon { width: 1.55em; height: 1.55em; }
}

@media (prefers-reduced-motion: reduce) {
  .msg-compose__send { transition: none; }
  .msg-compose__send:hover:not(:disabled),
  .msg-compose__send:active:not(:disabled) { transform: none; }
}

/* =====================================================================
   PHONE — a genuinely different layout, not a squeezed one.
   ===================================================================== */
@media (max-width: 44em) {
  /* One column. Both panes occupy the SAME cell; a class decides which is seen.
     No display:none dance across two grids, no duplicate markup. */
  .msg-main {
    grid-template-columns: 1fr;
    grid-template-areas: 'pane';
  }

  .msg-inbox, .msg-thread { grid-area: pane; }

  .msg-thread { display: none; }
  .msg-main.is-thread-open > .msg-inbox { display: none; }
  .msg-main.is-thread-open > .msg-thread { display: flex; }

  .msg-thread__back { display: inline-flex; }

  /* Roomier taps. A 2em icon button is under the ~44px minimum for a finger. */
  .msg-inbox__item { padding: var(--space-md); }
  .msg-bubble { max-width: 85%; }
  .msg-thread__list { padding: var(--space-md); }
  .msg-compose { padding-inline: var(--space-md); }
}

/* [TARGETED: 2026-07-17] The Contact button in the record detail modal —
   the link that stops /messages.html being an orphan page. */
.detail-contact {
  display: flex;
  justify-content: flex-end;   /* logical: mirrors under RTL */
  margin-block-start: var(--space-lg);
  padding-block-start: var(--space-md);
  border-block-start: 1px solid var(--border-light);
}

.detail-contact > .btn { display: inline-flex; align-items: center; gap: var(--space-sm); }

/* [TARGETED: 2026-07-17] My Services — a member's listing manager. */
.msvc-page { min-height: 100svh; min-height: 100vh; display: flex; flex-direction: column; }
.msvc-main { flex: 1; max-width: 44em; width: 100%; margin: 0 auto; padding: var(--space-lg); }

.msvc-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-md); margin-block-end: var(--space-lg);
}
.msvc-head > h1 { margin: 0; font-size: var(--text-lg); }

.msvc-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-sm); }

.msvc-item {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-md); flex-wrap: wrap;
  padding: var(--space-md);
  background: var(--bg-panel);
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
}
.msvc-item__main { display: flex; align-items: center; gap: var(--space-sm); flex-wrap: wrap; min-width: 0; }
.msvc-item__name { font-weight: 600; overflow-wrap: anywhere; }
.msvc-item__actions { display: flex; gap: var(--space-sm); flex-shrink: 0; }

/* Status pills — weight+colour, never colour alone. */
.msvc-status--pending { background: var(--natural-amber); color: var(--primary-fg); }
.msvc-status--archived { background: var(--bg-page); color: var(--text-secondary); }
.msvc-status--active { background: var(--natural-green); color: var(--on-strong); }

.msvc-input {
  width: 100%;
  /* 16px so iOS does not zoom on focus (documented, sticky behaviour). */
  font-size: 1rem;
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
  background: var(--bg-page);
  color: var(--text-primary);
}
.msvc-input:focus-visible { outline: none; border-color: var(--primary); }
textarea.msvc-input { resize: vertical; }

.msvc-check { width: 1.125em; height: 1.125em; }

/* Phone: actions drop below the name rather than crushing beside it. */
@media (max-width: 34em) {
  .msvc-item { flex-direction: column; align-items: stretch; }
  .msvc-item__actions { justify-content: flex-end; }
}

/* [TARGETED: 2026-07-17] Account destinations — Messages / My Services links.
   Mobile and desktop are shaped differently: on desktop they sit side by side
   as cards; on a phone they stack to full-width rows that are easy to tap. */
.account-nav {
  display: grid;
  /* auto-fit + minmax: two columns where there is room, one where there is not.
     No breakpoint to maintain — the grid decides from the available width. */
  grid-template-columns: repeat(auto-fit, minmax(min(14em, 100%), 1fr));
  gap: var(--space-md);
  margin-block-start: var(--space-lg);
}

.account-nav__item {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-lg);
  background: var(--bg-panel);
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
  color: var(--text-primary);
  text-decoration: none;
  font-weight: 500;
  /* The whole card lifts on hover — one hover language, matching .module-card. */
  transition: transform 0.15s ease-out, border-color 0.15s ease-out, box-shadow 0.15s ease-out;
}

.account-nav__item:hover {
  transform: translateY(-2px);
  border-color: var(--border-medium);
  box-shadow: 0 0.25em 0.75em rgba(0, 0, 0, 0.1);
}

.account-nav__item:focus-visible {
  outline: 0.125em solid var(--primary);
  outline-offset: 0.125em;
}

.account-nav__item > .icon { flex-shrink: 0; width: 1.5em; height: 1.5em; color: var(--primary); }
.account-nav__label { flex: 1; min-width: 0; }
/* margin-inline-start: pushes the badge to the end in BOTH writing directions. */
.account-nav__badge { margin-inline-start: auto; flex-shrink: 0; background: var(--primary); color: var(--primary-fg); }

/* Phone: bigger tap targets, always full-width rows. */
@media (max-width: 34em) {
  .account-nav { grid-template-columns: 1fr; }
  .account-nav__item { padding: var(--space-md); }
}

/* [TARGETED: 2026-07-17] SVG size constraints for message + service surfaces.
   A bare <svg> with no width/height defaults to 300x150px — the browser's
   replaced-element default. Several message/service icons are bare (no .icon
   class), so each gets an explicit em size here. All em, so they scale with the
   text they sit beside, and all sized in ONE place per surface. */

/* Message thread controls. */
.msg-thread__back > svg { width: 1.25em; height: 1.25em; }
.msg-thread__head .icon,
.msg-inbox__head .icon { width: 1.25em; height: 1.25em; }
/* The empty-state mark in the inbox. */
.msg-inbox .empty-state__icon { width: 2.5em; height: 2.5em; }

/* My Services controls — the add button, the close/edit icons, empty state. */
.msvc-page .btn > .icon { width: 1em; height: 1em; flex-shrink: 0; }
.msvc-page .btn-icon > svg { width: 1.125em; height: 1.125em; }
.msvc-page .empty-state__icon { width: 2.5em; height: 2.5em; }

/* Account destinations already size their .icon; the inline SVGs there inherit
   it. The badge and label stay em-sized so the row scales as one unit. */

/* THE SAFETY NET: any bare <svg> inside these surfaces that a rule above missed
   still gets a sane size instead of 300x150. :where() keeps it at specificity
   (0,0,1) so any explicit rule wins. */
:where(.msg-page, .msvc-page, .account-nav, .addlst-page) svg:not([width]):not(.icon) {
  width: 1.25em;
  height: 1.25em;
}

/* ===================================================================
   [TARGETED: 2026-07-19] Add-listing page — dedicated, device-adaptive,
   luxury composer. em units throughout; desktop = two columns (fields +
   image studio), mobile = one stacked column. Natural colours, subtle motion.
   =================================================================== */
.addlst-main {
  max-width: 64em;
  margin: 0 auto;
  padding: var(--space-lg);
}

/* The autosave chip — quiet proof the draft is remembered. */
.addlst-status {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--text-xs);
  color: var(--text-secondary);
  opacity: 0;
  transform: translateY(-0.25em);
  transition: opacity 0.25s ease-out, transform 0.25s ease-out;
  margin-bottom: var(--space-sm);
}
.addlst-status.is-shown { opacity: 1; transform: translateY(0); }
.addlst-status__icon { width: 1em; height: 1em; color: var(--natural-green, #22c55e); }

/* THE SKELETON. Mobile-first: a single column. */
.addlst-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-xl);
}
/* DESKTOP skeleton: two columns — fields flow left, the image studio is a wide
   sticky panel on the right. This is the different structure for the big screen. */
@media (min-width: 56em) {
  .addlst-grid { grid-template-columns: minmax(0, 1fr) 22em; align-items: start; }
  .addlst-images { position: sticky; top: var(--space-lg); }
}

.addlst-fields { display: flex; flex-direction: column; gap: var(--space-lg); }

.addlst-row { display: flex; flex-direction: column; gap: var(--space-xs); }
.addlst-row__label { font-weight: 500; font-size: var(--text-sm); }
.addlst-req { color: var(--natural-red, #ef4444); }
.addlst-row__help { font-size: var(--text-xs); color: var(--text-secondary); }

/* [TARGETED: 2026-07-19] PER-FIELD PRIVACY control (migration 039/040). Three
   segmented buttons — Public / Members / Private — under each field. Theme-token
   driven so it follows dark/light; em units; full borders (no "fresh nail cut");
   natural colours; subtle hover/active motion; RTL-safe via logical properties. */
.addlst-vis {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-xs); margin-block-start: var(--space-xs);
}
.addlst-vis__legend { font-size: var(--text-xs); color: var(--text-secondary); }
.addlst-vis__btns {
  display: inline-flex; border: 1px solid var(--border-default);
  border-radius: 0.375em; overflow: hidden;
}
.addlst-vis__btn {
  display: inline-flex; align-items: center; gap: 0.3em;
  padding: 0.3em 0.6em; font-size: var(--text-xs);
  background: var(--bg-panel); color: var(--text-secondary);
  border: none; border-inline-start: 1px solid var(--border-light);
  cursor: pointer; transition: background 0.15s ease-out, color 0.15s ease-out;
}
.addlst-vis__btn:first-child { border-inline-start: none; }
.addlst-vis__btn:hover { background: var(--muted); color: var(--text-primary); }
.addlst-vis__btn:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }
.addlst-vis__btn--active {
  background: var(--primary); color: var(--primary-fg); font-weight: 600;
}
.addlst-vis__btn--active:hover { background: var(--primary); color: var(--primary-fg); }
.addlst-vis__icon { width: 1em; height: 1em; }

.addlst-input {
  width: 100%;
  padding: 0.6em 0.75em;
  font-size: 1rem; /* 16px: stops iOS zoom-on-focus */
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
  background: var(--bg-panel);
  color: var(--text-primary);
  transition: border-color 0.15s ease-out, box-shadow 0.15s ease-out;
}
.addlst-input:hover { border-color: var(--border-medium, #9ca3af); }
.addlst-input:focus {
  outline: none;
  border-color: var(--accent-strong, #3b82f6);
  box-shadow: 0 0 0 0.2em color-mix(in oklab, var(--accent-strong, #3b82f6) 18%, transparent);
}
textarea.addlst-input { resize: vertical; min-height: 5em; line-height: 1.4; }

/* Custom checkbox — no browser default. */
.addlst-check { display: inline-flex; align-items: center; cursor: pointer; }
.addlst-check input { position: absolute; opacity: 0; width: 0; height: 0; }
.addlst-check__box {
  width: 1.25em; height: 1.25em;
  border: 1px solid var(--border-medium, #9ca3af);
  border-radius: 0.25em; background: var(--bg-panel);
  transition: all 0.15s ease-out; position: relative;
}
.addlst-check input:checked + .addlst-check__box {
  background: var(--accent-strong, #3b82f6); border-color: var(--accent-strong, #3b82f6);
}
.addlst-check input:checked + .addlst-check__box::after {
  content: ''; position: absolute; left: 0.42em; top: 0.18em;
  width: 0.28em; height: 0.55em; border: solid var(--on-strong); border-width: 0 0.14em 0.14em 0;
  transform: rotate(45deg);
}

/* ---- IMAGE STUDIO ---- */
.addlst-images {
  padding: var(--space-lg);
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
  background: var(--muted);
}
.addlst-images__title { font-size: var(--text-md); margin: 0 0 var(--space-xs); }
.addlst-images__hint { font-size: var(--text-xs); color: var(--text-secondary); margin: 0 0 var(--space-md); }

/* The drop zone: dashed on desktop (drag affordance), still tappable everywhere. */
.addlst-drop {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--space-xs);
  padding: var(--space-xl) var(--space-lg);
  border: 0.125em dashed var(--border-medium, #9ca3af);
  border-radius: var(--radius);
  background: var(--bg-panel);
  color: var(--text-secondary);
  cursor: pointer;
  text-align: center;
  transition: border-color 0.15s ease-out, background 0.15s ease-out, transform 0.15s ease-out;
}
.addlst-drop:hover, .addlst-drop:focus-visible { border-color: var(--accent-strong, #3b82f6); outline: none; }
.addlst-drop.is-dragover {
  border-color: var(--accent-strong, #3b82f6);
  background: color-mix(in oklab, var(--accent-strong, #3b82f6) 8%, var(--bg-panel));
  transform: scale(1.01);
}
.addlst-drop__icon { width: 2.5em; height: 2.5em; color: var(--natural-slate, #64748b); }
.addlst-drop__lead { font-weight: 500; font-size: var(--text-sm); color: var(--text-primary); }
.addlst-drop__or { font-size: var(--text-xs); }
/* On touch (no hover), lead with "tap to choose" — the drag line is desktop's. */
@media (hover: none) {
  .addlst-drop__lead { display: none; }
  .addlst-drop__or::before { content: '＋ '; }
}

/* Per-file thumbnails, each with its own state. */
.addlst-thumbs {
  list-style: none; margin: var(--space-md) 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(4.5em, 1fr)); gap: var(--space-sm);
}
.addlst-thumb {
  position: relative; aspect-ratio: 1; border-radius: 0.375em; overflow: hidden;
  border: 1px solid var(--border-default); background: var(--bg-panel);
}
.addlst-thumb__img { width: 100%; height: 100%; object-fit: cover; display: block; }
.addlst-thumb__del {
  position: absolute; top: 0.25em; inset-inline-end: 0.25em;
  width: 1.5em; height: 1.5em; display: flex; align-items: center; justify-content: center;
  border: none; border-radius: 50%; background: var(--overlay-scrim); color: var(--on-strong); cursor: pointer;
  transition: background 0.15s ease-out, transform 0.15s ease-out;
}
.addlst-thumb__del:hover { background: var(--natural-red, #ef4444); transform: scale(1.08); }
/* [TARGETED: 2026-07-19] The pending tile now shows the REAL image (blob URL)
   with the progress bar OVERLAID at the bottom — the modern "preview instantly,
   fill as it uploads" pattern, not a placeholder glyph. A gentle dim signals
   "still uploading"; it clears the moment the tile swaps to the stored image. */
.addlst-thumb--pending .addlst-thumb__img { filter: brightness(0.82); }
.addlst-thumb__overlay {
  position: absolute; inset-inline: 0; bottom: 0;
  height: 0.35em; background: rgba(0, 0, 0, 0.35);
}
.addlst-thumb__fill {
  display: block; height: 100%;
  background: var(--accent-strong, #3b82f6);
  transition: width 0.2s ease-out;
}
/* The graceful fallback glyph — only when the browser truly cannot decode. */
.addlst-thumb__ph {
  width: 100%; height: 100%;
  display: flex; align-items: center; justify-content: center;
  color: var(--natural-slate, #64748b);
}

/* [TARGETED: 2026-07-19] FAILED-upload state + one-click RETRY. On failure the
   tile keeps its preview (dimmed red) and overlays a Retry button — the 2025
   standard "specific error + one-click retry", no re-selection needed. */
.addlst-thumb--failed .addlst-thumb__img { filter: brightness(0.55) saturate(0.7); }
.addlst-thumb__retry {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.15em;
  border: none; cursor: pointer;
  background: color-mix(in oklab, var(--natural-red) 55%, transparent);
  color: var(--on-strong); font-size: var(--text-xs); font-weight: 600;
  transition: background 0.15s ease-out;
}
.addlst-thumb__retry:hover { background: color-mix(in oklab, var(--natural-red, #ef4444) 72%, transparent); }
.addlst-thumb__retry:focus-visible { outline: 0.125em solid var(--on-strong); outline-offset: -0.1875em; }

/* Sticky action bar — always reachable, thumb-friendly on mobile. */
.addlst-actions {
  /* [TARGETED: 2026-07-19] DARK/LIGHT THEME FIX. This sticky bar (holding the
     Save-draft + Publish buttons) used `background: var(--bg, var(--surface, #fff))`
     — but neither --bg nor --surface is a real theme token, so it fell through to
     hardcoded #fff and stayed WHITE in dark mode. The buttons themselves were
     fine; the CONTAINER was the culprit. --bg-panel is the theme-aware token every
     card/panel uses, so the bar now follows light/dark/default. Same fix applied
     to all 11 --surface/--bg fallbacks across the addlst/stats/contact-cta CSS. */
  display: flex; justify-content: flex-end; gap: var(--space-sm);
  margin-top: var(--space-xl);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--border-default);
  position: sticky; bottom: 0;
  background: var(--bg-panel);
  padding-bottom: max(var(--space-md), env(safe-area-inset-bottom));
}
@media (max-width: 40em) {
  .addlst-actions { flex-direction: column-reverse; }
  .addlst-actions .btn { width: 100%; justify-content: center; }
}
.addlst-page .btn > .icon { width: 1em; height: 1em; }

/* [TARGETED: 2026-07-19] GEO FIELD — the map-pin picker widget.
   Chrome (toolbar / coordinate inputs / status) is styled with app tokens in em
   units, full natural-colour borders, and hover/transition on every action
   element. Leaflet's own stylesheet handles the map surface; the rules under
   `.geo-field__map .leaflet-*` only re-skin its controls to match the app and
   fix the marker z-index inside the app's stacking context. Logical properties
   throughout, so it mirrors correctly in RTL (ar/ur). */
.geo-field { display: flex; flex-direction: column; gap: var(--space-sm); }
.geo-field__hint { margin: 0; font-size: var(--text-xs); color: var(--text-secondary); line-height: 1.4; }

.geo-field__toolbar { display: flex; flex-wrap: wrap; gap: var(--space-sm); }
.geo-field__locate, .geo-field__clear {
  display: inline-flex; align-items: center; gap: var(--space-xs);
  font-size: var(--text-sm);
}
.geo-field__locate .icon, .geo-field__clear .icon { width: 1.05em; height: 1.05em; }
/* [TARGETED: 2026-07-21] FEATURE-GEO-LOCATE-ICON: the shared `.icon` rule sets
   `fill: none; stroke: currentColor` for the app's usual single-color outline
   icons, which would otherwise hide this icon's two literal per-path fills.
   This is a deliberate two-tone exception (dark pin body + red accent
   circle) — scoped to override `.icon` ONLY for this specific icon's own
   child elements, so every other icon in the app keeps its normal outline
   styling untouched. Colors match the inline fill="#212121"/"#f44336"
   attributes exactly (not tokenized to this app's OKLCH palette) since the
   user approved this exact look verbatim. */
.geo-field__locate-icon { stroke: none; }
.geo-field__locate-icon path { fill: #212121; }
.geo-field__locate-icon circle { fill: #f44336; }
.geo-field__locate[disabled] { opacity: 0.6; cursor: progress; }
/* The locate icon gently pulses while a fix is in progress (disabled state). */
.geo-field__locate[disabled] .geo-field__locate-icon { animation: geoPulse 1.2s ease-in-out infinite; }
@keyframes geoPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
@media (prefers-reduced-motion: reduce) {
  .geo-field__locate[disabled] .geo-field__locate-icon { animation: none; }
}

.geo-field__map {
  width: 100%; height: 20em;
  border: 0.0625em solid var(--border-default);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--muted);
  transition: border-color var(--duration-fast) var(--ease-standard);
}
.geo-field__map:focus-within { border-color: var(--border-medium); }
@media (max-width: 40em) { .geo-field__map { height: 16em; } }
/* [TARGETED: 2026-07-20] Touch "tap to activate" hint. On a phone the map starts
   with panning locked so a scrolling finger passes through; this ribbon tells the
   user to tap once to interact. It sits at the bottom, does not block the tap
   (pointer-events:none), and disappears the moment dragging is enabled. */
.geo-field__map--locked { position: relative; }
.geo-field__map--locked::after {
  content: attr(data-geo-locked-hint);
  position: absolute; inset-inline: 0; inset-block-end: 0;
  padding: var(--space-xs) var(--space-sm);
  font-size: var(--text-xs); text-align: center;
  color: var(--text-primary);
  background: color-mix(in oklch, var(--bg-panel) 82%, transparent);
  border-block-start: 0.0625em solid var(--border-light);
  pointer-events: none;
}

.geo-field__status {
  margin: 0; font-size: var(--text-xs); line-height: 1.45;
  color: var(--text-secondary);
  border: 0.0625em solid var(--border-light);
  border-radius: var(--radius);
  padding: var(--space-sm) var(--space-md);
  background: var(--muted);
}
.geo-field__status--error {
  color: var(--status-bad-fg);
  border-color: color-mix(in oklch, var(--status-bad-fg) 40%, var(--border-default));
  background: var(--status-bad-bg);
}

.geo-field__coords {
  border: 0.0625em solid var(--border-default);
  border-radius: var(--radius);
  padding: var(--space-md);
  margin: 0;
}
/* [TARGETED: 2026-07-20] Coordinate inputs hidden by request — only the map frame
   (with its pin) is shown. The fieldset stays in the DOM and its inputs keep
   holding the value the map writes; display:none simply takes it off-screen. The
   JS reads/writes input.value directly (not via focus or form submit), so hiding
   it does not break the map<->value sync. Remove this class to bring the manual
   coordinate entry back. */
.geo-field__coords--hidden { display: none; }
.geo-field__coords-legend {
  font-size: var(--text-xs); font-weight: 500; color: var(--text-secondary);
  padding-inline: var(--space-xs);
}
.geo-field__coords-grid {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-sm);
}
@media (max-width: 30em) { .geo-field__coords-grid { grid-template-columns: 1fr; } }
.geo-field__coord { display: flex; flex-direction: column; gap: var(--space-xs); }
.geo-field__coord-label { font-size: var(--text-xs); color: var(--text-secondary); }
.geo-field__coord-input {
  font: inherit; font-size: var(--text-sm);
  padding: 0.45em 0.6em;
  border: 0.0625em solid var(--border-default);
  border-radius: 0.375em;
  background: var(--bg-panel); color: var(--text-primary);
  transition: border-color var(--duration-fast) var(--ease-standard),
              box-shadow var(--duration-fast) var(--ease-standard);
}
.geo-field__coord-input:hover { border-color: var(--border-medium); }
.geo-field__coord-input:focus {
  outline: none; border-color: var(--primary);
  box-shadow: 0 0 0 0.1875em color-mix(in oklch, var(--primary) 22%, transparent);
}

/* Re-skin Leaflet's controls to the app palette; keep its geometry. */
.geo-field__map .leaflet-bar a {
  background: var(--bg-panel); color: var(--text-primary);
  border-bottom-color: var(--border-default);
  transition: background var(--duration-fast) var(--ease-standard);
}
.geo-field__map .leaflet-bar a:hover { background: var(--muted); }
.geo-field__map .leaflet-control-attribution {
  background: color-mix(in oklch, var(--bg-panel) 82%, transparent);
  color: var(--text-secondary);
}
.geo-field__map .leaflet-control-attribution a { color: var(--primary); }

/* Read-only display (record detail / cards). A compact coordinate line + a
   real embedded, read-only Leaflet map (geo-display-map.js). The <noscript>
   "view on map" OSM link only ever renders with JS disabled — see
   record-render.js's renderGeoValue. */
.geo-display { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-xs); width: 100%; max-width: 28em; }
.geo-display__coords { font-variant-numeric: tabular-nums; }
/* [TARGETED: 2026-07-21] UI-0002 — sized like .geo-field__map (the picker) but
   shorter: this is a fixed-location preview, not an editable working area.
   dragging/scroll/zoom are ALSO disabled in JS (geo-display-map.js) — the
   pointer-events:none guard here is a second, CSS-only layer in case a
   Leaflet control renders before JS finishes wiring the read-only options,
   and it never blocks the map from being visible, just from being dragged. */
.geo-display__map {
  width: 100%; height: 12em;
  border: 0.0625em solid var(--border-default);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--muted);
}
@media (max-width: 40em) { .geo-display__map { height: 10em; } }
.geo-display__link {
  display: inline-flex; align-items: center; gap: 0.25em;
  color: var(--primary); text-decoration: none; font-size: var(--text-xs);
  transition: opacity var(--duration-fast) var(--ease-standard);
}
.geo-display__link:hover { opacity: 0.78; text-decoration: underline; text-underline-offset: 0.15em; }
.geo-display__link .icon { width: 0.95em; height: 0.95em; }
.addlst-row--geo { gap: var(--space-sm); }

/* [TARGETED: 2026-07-17] iOS 26 Liquid Glass — the actual glass, gated on support.
   @supports so ONLY a browser that can blur gets translucency. Everything above
   already gave non-supporting browsers a solid opaque panel; this upgrades the
   supporting ~95% (caniuse: backdrop-filter Baseline since Sept 2024). */
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .public-menu__panel {
    /* Translucent so the blurred backdrop shows through — the essence of glass.
       color-mix keeps it theme-aware: light panel in light mode, dark in dark. */
    background: color-mix(in oklch, var(--bg-panel) 72%, transparent);
    /* saturate boosts colour behind the blur so it looks like frosted glass, not
       grey. -webkit- prefix first: Safari still needs it in 2026. */
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
    backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
    /* A hairline that reads as a glass rim rather than a hard border. */
    border-color: color-mix(in oklch, var(--border-default) 60%, transparent);
  }
}

/* Continuous-corner children: menu items get the concentric radius (outer minus
   padding) so a hovered item's rounding nests inside the panel's, the iOS look. */
.public-menu__item { border-radius: var(--glass-radius-item); }

.public-menu__item:hover {
  /* On glass, a hovered row is a brighter pane, not a flat grey fill. */
  background: color-mix(in oklch, var(--text-primary) 8%, transparent);
}

@keyframes glassOpen {
  from { opacity: 0; transform: scale(0.94); }
  to   { opacity: 1; transform: scale(1); }
}

/* ACCESSIBILITY — both are documented iOS/HIG concerns, mapped to their web
   equivalents.

   reduce-transparency: iOS lets users turn OFF translucency (Settings >
   Accessibility). The web equivalent is prefers-reduced-transparency. When set,
   drop the glass entirely for a solid panel — the blur is decoration, legibility
   is not. (Experimental: ~73% support, zero Safari — so this HELPS where it
   works and changes nothing where it does not.) */
@media (prefers-reduced-transparency: reduce) {
  .public-menu__panel {
    background: var(--bg-panel);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* reduce-motion: the spring open is motion. Someone who asked for less gets an
   instant panel — the menu still works identically, it just does not animate. */
@media (prefers-reduced-motion: reduce) {
  .public-menu__panel { animation: none; }
}

/* ===================================================================
   [TARGETED: 2026-07-19] STATISTICS DASHBOARD. Self-contained SVG charts,
   theme-aware (CSS vars), em-based, responsive. No external chart lib.
   =================================================================== */
/* [TARGETED: 2026-07-19] Mobile overflow fix ("blocks out of page on mobile").
   The dashboard and its grid children must never exceed the viewport width. A
   grid/flex child defaults to min-width:auto (content-based), so a long number or
   a wide chart could push a card past the screen edge — min-width:0 lets it shrink.
   overflow-x:hidden on the dash is the belt-and-braces guard so nothing bleeds. */
.stats-dash { max-width: 72em; margin: 0 auto; min-width: 0; overflow-x: hidden; }
.stats-kpis > .stats-kpi, .stats-grid > .stats-card { min-width: 0; }
.stats-kpi__value { overflow-wrap: anywhere; }   /* a huge number wraps, never overflows */
.stats-card { min-width: 0; }
/* Charts + tables already scroll inside their own container; ensure the SVG can
   shrink to the card width on a narrow phone. */
.stats-svg { max-width: 100%; }
.stats-dash__head {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: var(--space-md); margin-bottom: var(--space-lg);
}
.stats-range { display: inline-flex; align-items: center; gap: var(--space-sm); font-size: var(--text-sm); }
.stats-range select {
  padding: 0.4em 0.6em; border: 1px solid var(--border-default); border-radius: var(--radius);
  background: var(--bg-panel); color: var(--text-primary); font-size: 1rem;
}

/* KPI cards — the headline numbers. */
.stats-kpis {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(9em, 1fr));
  gap: var(--space-md); margin-bottom: var(--space-lg);
}
.stats-kpi {
  display: flex; flex-direction: column; gap: 0.1em;
  padding: var(--space-md); border: 1px solid var(--border-default);
  border-radius: var(--radius); background: var(--bg-panel);
  transition: border-color 0.15s ease-out, transform 0.15s ease-out;
}
.stats-kpi:hover { border-color: var(--border-medium, #9ca3af); transform: translateY(-0.1em); }
.stats-kpi__value { font-size: 1.8rem; font-weight: 600; line-height: 1.1; color: var(--text-primary); }
.stats-kpi__label { font-size: var(--text-xs); color: var(--text-secondary); }
.stats-kpi__sub { font-size: var(--text-xs); color: var(--text-secondary); opacity: 0.8; }

/* Chart cards + two-column grid that stacks on phones. */
.stats-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-lg); margin-bottom: var(--space-lg); }
@media (min-width: 48em) { .stats-grid { grid-template-columns: 1fr 1fr; } }
.stats-card {
  padding: var(--space-lg); border: 1px solid var(--border-default);
  border-radius: var(--radius); background: var(--bg-panel); margin-bottom: var(--space-lg);
}
.stats-grid > .stats-card { margin-bottom: 0; }
.stats-card__title { font-size: var(--text-md); margin: 0 0 var(--space-md); }
.stats-chart { width: 100%; }

/* The SVG charts. font-size on text via CSS since SVG text uses em of the svg. */
.stats-svg { width: 100%; height: auto; overflow: visible; }
.stats-svg--line { height: 10em; }
.stats-svg__label { font-size: 1.6px; fill: var(--text-secondary); }
.stats-svg__value { font-size: 1.5px; fill: var(--text-primary); font-weight: 600; }
.stats-line__peak { font-size: var(--text-xs); color: var(--text-secondary); text-align: end; margin-top: 0.25em; }
.stats-empty { font-size: var(--text-sm); color: var(--text-secondary); font-style: italic; }

/* Funnel — two stacked proportional bars + conversion rate. */
.stats-funnel { display: flex; flex-direction: column; gap: var(--space-sm); }
/* min-width:0 lets the 1fr bar column shrink below its content on a narrow phone
   instead of forcing the row wider than the screen. */
.stats-funnel__row { display: grid; grid-template-columns: 12em 1fr auto; align-items: center; gap: var(--space-sm); font-size: var(--text-sm); min-width: 0; }
.stats-funnel__row > * { min-width: 0; }
.stats-funnel__k { color: var(--text-secondary); overflow-wrap: anywhere; }
.stats-funnel__bar { height: 1.4em; border-radius: 0.3em; background: var(--primary); width: var(--w, 0%); transition: width 0.4s ease-out; }
/* [TARGETED: 2026-07-19] Tokenised (was a hardcoded #22c55e — "var must be used").
   --2 = the "good/approved" green, --bad = the rejected red, from status tokens. */
.stats-funnel__bar--2 { background: var(--status-ok-fg); }
.stats-funnel__bar--bad { background: var(--status-bad-fg); }
.stats-funnel__v { font-weight: 600; min-width: 3em; text-align: end; }
.stats-funnel__rate { margin-top: var(--space-sm); font-weight: 600; color: var(--text-primary); }
@media (max-width: 34em) { .stats-funnel__row { grid-template-columns: 1fr auto; } .stats-funnel__k { grid-column: 1 / -1; } }

/* Matrix + tables — scroll horizontally inside their own box, never the page. */
.stats-matrix-wrap, .stats-table-wrap { overflow-x: auto; }
.stats-matrix, .stats-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
.stats-matrix th, .stats-matrix td, .stats-table th, .stats-table td {
  padding: 0.5em 0.75em; text-align: start; border-bottom: 1px solid var(--border-light, var(--border-default));
}
.stats-matrix th, .stats-table th { font-weight: 600; color: var(--text-secondary); font-size: var(--text-xs); }
.stats-matrix__total td { font-weight: 700; border-top: 2px solid var(--border-default); }
.stats-table tbody tr:hover, .stats-matrix tbody tr:hover { background: color-mix(in oklab, var(--text-primary) 4%, transparent); }
/* Matrix cells: numeric right-align, a row-head + row-total emphasis, and a
   subtle heat tint driven by --heat (0..1) so the busiest device×event pops. */
.stats-matrix__cell, .stats-matrix__rowtotal, .stats-matrix td:not(.stats-matrix__rowhead) { text-align: end; font-variant-numeric: tabular-nums; }
.stats-matrix__rowhead { font-weight: 600; }
.stats-matrix__rowtotal { font-weight: 600; }
.stats-matrix__cell[style*="--heat"] {
  background: color-mix(in oklab, var(--accent-strong, #3b82f6) calc(var(--heat, 0) * 28%), transparent);
}
.stats-matrix th:not(:first-child) { text-align: end; }

/* [TARGETED: 2026-07-19] PROFILES PERMISSION MATRIX — was PHANTOM (used in
   admin-profiles.ejs, zero CSS), so the wide Field×View×Edit table rendered
   unstyled AND could overflow the screen on a phone with no scroll container.
   Overflow containment is the priority; the rest matches the data-table style. */
.matrix-scroll {
  overflow-x: auto;                 /* the wide table scrolls in its OWN box */
  -webkit-overflow-scrolling: touch;
  border: 1px solid var(--border-default);
  border-radius: var(--radius);
}
.matrix {
  width: 100%;
  min-width: 22em;                  /* below this it scrolls rather than crushes */
  border-collapse: collapse;
  font-size: var(--text-sm);
}
.matrix th, .matrix td {
  padding: 0.5em 0.75em;
  text-align: start;
  border-bottom: 1px solid var(--border-light, var(--border-default));
}
.matrix th { font-weight: 600; color: var(--text-secondary); font-size: var(--text-xs); }
.matrix tbody tr:hover { background: color-mix(in oklab, var(--text-primary) 4%, transparent); }
.matrix .col-toggle { text-align: center; white-space: nowrap; }
.bulk-btn {
  padding: 0.15em 0.5em;
  margin: 0 0.1em;
  font-size: var(--text-xs);
  border: 1px solid var(--border-default);
  border-radius: 0.25em;
  background: var(--bg-panel);
  color: var(--text-secondary);
  cursor: pointer;
  transition: border-color 0.15s ease-out, color 0.15s ease-out, background 0.15s ease-out;
}
.bulk-btn:hover { border-color: var(--accent-strong, #3b82f6); color: var(--text-primary); }

/* [TARGETED: 2026-07-19] users-table / skills-table were PHANTOM wrappers around
   a raw <table> — no overflow containment, so on a phone the wide admin tables
   (username·email·status·date·actions) pushed past the screen. Give them the
   same scroll-in-own-box treatment as .card-table-wrap. */
.users-table, .skills-table { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.users-table > table, .skills-table > table {
  width: 100%; min-width: 24em; border-collapse: collapse; font-size: var(--text-sm);
}
.users-table th, .users-table td, .skills-table th, .skills-table td {
  padding: 0.5em 0.75em; text-align: start; border-bottom: 1px solid var(--border-light, var(--border-default));
}
.users-table th, .skills-table th { font-weight: 600; color: var(--text-secondary); font-size: var(--text-xs); }
.users-table tbody tr:hover, .skills-table tbody tr:hover { background: color-mix(in oklab, var(--text-primary) 4%, transparent); }

/* [TARGETED: 2026-07-19] Field-exposure posture bar — 3 proportional segments
   (public / member-only / admin-only) so an over-exposed module is obvious.
   Natural colours: public = warm (widest visibility = most attention), member =
   accent, admin = neutral slate. */
.stats-expo__bar {
  display: inline-flex; width: 100%; min-width: 6em; height: 0.9em;
  border-radius: 0.45em; overflow: hidden; background: var(--muted);
}
.stats-expo__seg { height: 100%; }
.stats-expo__seg--public { background: var(--natural-amber); }   /* the public internet sees these */
.stats-expo__seg--member { background: var(--accent-strong, #3b82f6); }
.stats-expo__seg--admin  { background: var(--natural-slate); }
.stats-exposure td:not(.stats-matrix__rowhead) { text-align: end; font-variant-numeric: tabular-nums; }
.stats-exposure td:last-child { text-align: start; min-width: 8em; }

/* [TARGETED: 2026-07-19] PENDING-APPROVAL card + banner. Amber "pending" (reuses
   the theme-aware --status-warn-* pair — not red, not green; WCAG-safe with the
   clock icon + text, never colour alone). FULL border all sides (no nail-cut).
   Tokens + em throughout; logical properties mirror it in RTL; entrance animation
   is opt-in under prefers-reduced-motion: no-preference. */
.pending-card {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: var(--space-sm); max-width: 30em; margin: var(--space-lg) auto;
  padding: var(--space-lg) var(--space-lg) var(--space-xl);
  border: 0.0625em solid var(--status-warn-fg);          /* full border, all sides */
  border-radius: 0.75em;
  background: var(--status-warn-bg);
  color: var(--status-warn-fg);
}
.pending-card__icon { display: inline-flex; }
.pending-card__icon > svg { width: 2.5em; height: 2.5em; }
.pending-card__title { margin: 0; font-size: var(--text-lg); color: var(--text-primary); }
.pending-card__body { margin: 0; font-size: var(--text-sm); color: var(--text-primary); line-height: 1.5; }
.pending-card__cta { margin-block-start: var(--space-sm); }

@media (prefers-reduced-motion: no-preference) {
  .pending-card { animation: pending-in 0.2s var(--ease-standard, ease-out) both; }
  @keyframes pending-in { from { opacity: 0; transform: translateY(0.5em); } to { opacity: 1; transform: none; } }
}

/* The persistent "awaiting approval" banner on a pending listing in My Listings.
   A compact inline strip: clock icon + label + hint. Full border, amber tint. */
.pending-banner {
  display: flex; align-items: center; gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md); margin-block: var(--space-xs);
  border: 0.0625em solid var(--status-warn-fg);
  border-radius: 0.5em;
  background: var(--status-warn-bg); color: var(--status-warn-fg);
  font-size: var(--text-xs);
}
.pending-banner__icon { flex-shrink: 0; display: inline-flex; }
.pending-banner__icon > svg { width: 1.1em; height: 1.1em; }
.pending-banner__text { color: var(--text-primary); }
.pending-banner__text strong { font-weight: 600; }
.pending-banner__hint { color: var(--text-secondary); }
/* Rejected variant reuses the "bad" status pair (still icon + text, not colour-alone). */
.pending-banner--rejected { border-color: var(--status-bad-fg); background: var(--status-bad-bg); color: var(--status-bad-fg); }

/* ===========================================================================
   MODERATION QUEUE (admin/moderation) — pending member listings + approve/reject.
   Backend is src/moderation.js + the /api/admin/moderation/* routes (untouched
   by this page). Amber "pending" accent reuses the theme-aware --status-warn-*
   pair, same convention as .pending-card/.pending-banner above. FULL borders on
   every surface (never a left-only accent). em units + tokens throughout;
   logical properties (inline/block) keep it correct under RTL languages (ar, ur).
   =========================================================================== */
.modq { display: flex; flex-direction: column; gap: var(--space-lg); max-width: 56em; margin-inline: auto; }

.modq__head { display: flex; flex-direction: column; gap: var(--space-xs); }
.modq__title { display: flex; align-items: center; gap: var(--space-sm); }
.modq__title > h2 { margin: 0; font-size: var(--text-lg); color: var(--text-primary); }
.modq__count {
  font-size: var(--text-xs); font-weight: 600; color: var(--status-warn-fg);
  background: var(--status-warn-bg); border: 1px solid var(--status-warn-fg);
  border-radius: 1em; padding: 0.125em 0.625em; min-width: 1.5em; text-align: center;
}
.modq__subtitle { margin: 0; font-size: var(--text-sm); color: var(--text-secondary); }

/* --- loading skeleton: pulsing shimmer rows, same shimmer keyframe family as
   the messages inbox skeleton, restated here so this page has no cross-file
   dependency on public-messages.js loading first. --- */
.modq__skeleton { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-md); }
.modq__skel-row {
  display: flex; align-items: center; gap: var(--space-md);
  padding: var(--space-md); border: 1px solid var(--border-light); border-radius: var(--radius);
  background: var(--bg-panel);
}
.modq__skel-lines { flex: 1; display: flex; flex-direction: column; gap: var(--space-sm); }
.modq__skel {
  display: block; border-radius: 0.3em;
  background: linear-gradient(90deg, var(--muted) 25%, var(--border-default) 37%, var(--muted) 63%);
  background-size: 400% 100%;
  animation: modq-shimmer 1.4s ease-in-out infinite;
}
.modq__skel--thumb { width: 4.5em; height: 4.5em; flex-shrink: 0; border-radius: 0.375em; }
.modq__skel--line-lg { width: 60%; height: 1em; }
.modq__skel--line-sm { width: 40%; height: 0.8em; }
@media (prefers-reduced-motion: reduce) { .modq__skel { animation: none; } }
@keyframes modq-shimmer { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }

/* --- the queue itself --- */
.modq__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-md); }

.modq__item {
  display: flex; align-items: flex-start; gap: var(--space-lg);
  padding: var(--space-lg);
  background: var(--bg-panel);
  border: 1px solid var(--status-warn-fg);   /* full border, all sides, amber "pending" accent */
  border-radius: var(--radius);
  transition: box-shadow 0.2s ease-out, transform 0.2s ease-out, opacity 0.2s ease-out;
}
.modq__item:hover { box-shadow: var(--shadow-md, 0 0.25em 0.75em rgba(0, 0, 0, 0.08)); transform: translateY(-0.125em); }
.modq__item--leaving { opacity: 0; transform: translateY(-0.25em) scale(0.98); }

/* thumbnail: fixed square frame so a mixed list of image / no-image cards stays
   visually even. Level 3 nesting: .modq__item > .modq__item-thumb > img. */
.modq__item-thumb {
  flex-shrink: 0; width: 5em; height: 5em; overflow: hidden;
  border: 1px solid var(--border-default); border-radius: 0.5em;
  background: var(--muted);
  display: flex; align-items: center; justify-content: center;
}
.modq__item-thumb-img { width: 100%; height: 100%; object-fit: cover; display: block; }
.modq__item-thumb-placeholder { display: inline-flex; color: var(--text-secondary); }
.modq__item-thumb-placeholder > .icon { width: 1.75em; height: 1.75em; }

/* meta column: title row, detail list, inline message, actions — each its own
   nested block so the item stays readable at 3+ levels deep. */
.modq__item-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--space-sm); }

.modq__item-title-row { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-sm); }
.modq__item-title {
  font-size: var(--text-md); font-weight: 600; color: var(--text-primary);
  overflow-wrap: anywhere;
}
.modq__badge {
  display: inline-flex; align-items: center; gap: 0.25em;
  font-size: var(--text-xs); font-weight: 500;
  border: 1px solid transparent; border-radius: 1em;
  padding: 0.125em 0.625em;
}
.modq__badge--pending {
  color: var(--status-warn-fg); background: var(--status-warn-bg); border-color: var(--status-warn-fg);
}

.modq__item-details {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(11em, 1fr));
  gap: var(--space-xs) var(--space-lg);
  margin: 0; font-size: var(--text-sm);
}
.modq__item-detail { display: flex; flex-direction: column; gap: 0.125em; }
.modq__item-detail > dt {
  font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.03em;
  color: var(--text-secondary);
}
.modq__item-detail > dd { margin: 0; color: var(--text-primary); }

.modq__item-msg {
  font-size: var(--text-xs); padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--status-bad-fg); border-radius: 0.375em;
  background: var(--status-bad-bg); color: var(--status-bad-fg);
}

.modq__item-actions { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin-block-start: var(--space-xs); }
.modq__action { font-size: var(--text-sm); }
.modq__action .icon { width: 1em; height: 1em; }
.modq__action:disabled { opacity: 0.6; cursor: not-allowed; transform: none; box-shadow: none; }

/* --- empty state: nothing pending. Self-contained (base.css has no shared
   .empty-state layout to lean on), full nested structure, natural stone icon. --- */
.modq__empty {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: var(--space-sm); padding: var(--space-xl) var(--space-lg);
  border: 1px solid var(--border-default); border-radius: var(--radius);
  background: var(--bg-panel); color: var(--text-secondary);
}
.modq__empty-icon { width: 2.5em; height: 2.5em; color: var(--border-medium); stroke-width: 1; fill: none; stroke: currentColor; }
.modq__empty-title { font-size: var(--text-md); font-weight: 500; color: var(--text-primary); }
.modq__empty-hint { font-size: var(--text-sm); max-width: 26em; }

/* --- error state: the initial fetch failed outright --- */
.modq__error {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: var(--space-sm); padding: var(--space-xl) var(--space-lg);
  border: 1px solid var(--status-bad-fg); border-radius: var(--radius);
  background: var(--status-bad-bg); color: var(--status-bad-fg);
}
.modq__error-icon { width: 2.25em; height: 2.25em; stroke-width: 1; fill: none; stroke: currentColor; }
.modq__error-text { font-size: var(--text-sm); }

@media (prefers-reduced-motion: no-preference) {
  .modq__item { animation: modq-item-in 0.2s var(--ease-standard, ease-out) both; }
  @keyframes modq-item-in { from { opacity: 0; transform: translateY(0.4em); } to { opacity: 1; transform: none; } }
}

/* =====================================================================
   [TARGETED: 2026-07-19] TWO DISTINCT layouts for the approval queue — the
   MOBILE card and the DESKTOP dense row are genuinely different renderings of
   the same semantic markup, not one layout that reflows. Split on a real
   interaction/width query, not width alone: fine-pointer + wide = desktop.

   The base rules above define the MOBILE-FIRST card (stacked). The desktop
   block below transforms it into a compact console row. Every value a token/em;
   full borders; the amber pending accent is a full 4-side border (no nail-cut).
   ===================================================================== */

/* ---------- MOBILE (default / narrow / touch): a STACKED CARD ----------
   Big thumbnail banner on top, title + status, details stacked, and full-width
   thumb-friendly action buttons at the foot. This is the base; desktop overrides. */
.modq__item { flex-direction: column; gap: var(--space-md); padding: var(--space-md); }
.modq__item-thumb { width: 100%; height: 9em; }        /* banner thumbnail on mobile */
.modq__item-meta { width: 100%; }
.modq__item-details { grid-template-columns: 1fr 1fr; } /* two compact columns of detail */
.modq__item-actions { width: 100%; }
.modq__item-actions > .modq__action { flex: 1; justify-content: center; min-height: 2.75em; } /* ~44px tap targets */

/* ---------- DESKTOP (wide + fine pointer): a DENSE CONSOLE ROW ----------
   Small square thumbnail at the inline-start, the title + inline details taking
   the middle, and the actions pinned to the trailing edge — scannable like a
   moderation console. A distinct layout, own rules. */
@media (min-width: 48em) and (pointer: fine) {
  .modq__item {
    flex-direction: row; align-items: center; gap: var(--space-lg);
    padding: var(--space-md) var(--space-lg);
  }
  .modq__item-thumb { width: 3.5em; height: 3.5em; }    /* compact square, not a banner */
  .modq__item-meta {
    flex: 1; min-width: 0;
    display: grid; grid-template-columns: 1fr auto; align-items: center;
    gap: 0 var(--space-lg);
  }
  /* title row spans, details sit inline beneath — a tight two-line band */
  .modq__item-title-row { grid-column: 1; }
  .modq__item-details {
    grid-column: 1; display: flex; flex-wrap: wrap; gap: 0 var(--space-lg);
    margin-top: 0.15em;
  }
  .modq__item-detail { flex-direction: row; align-items: baseline; gap: 0.35em; }
  .modq__item-detail > dt::after { content: ':'; }      /* inline "Label: value" */
  /* actions pinned to the trailing edge of the row, not full width */
  .modq__item-actions {
    grid-column: 2; grid-row: 1 / 3; width: auto; flex-wrap: nowrap;
    align-self: center; margin-block-start: 0;
  }
  .modq__item-actions > .modq__action { min-height: 0; }
  /* the inline per-item message drops to its own line spanning the meta */
  .modq__item-msg { grid-column: 1 / -1; }
}

/* [TARGETED: 2026-07-19] Admin CAPABILITIES panel (Profiles page). Per profile,
   a labelled toggle per capability. Tokens + em, full borders, sized SVG. */
.capability-panel__hint { font-size: var(--text-sm); color: var(--text-secondary); margin: 0 0 var(--space-md); }
.capability-panel__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-md); }
.capability-panel__profile {
  border: 0.0625em solid var(--border-default); border-radius: 0.5em;
  padding: var(--space-md); background: var(--bg-panel);
}
.capability-panel__profile-name { font-weight: 600; font-size: var(--text-sm); color: var(--text-primary); }
.capability-panel__caps { list-style: none; margin: var(--space-sm) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-sm); }
.capability-toggle { display: flex; align-items: flex-start; gap: var(--space-sm); cursor: pointer; }
.capability-toggle__label { display: flex; flex-direction: column; gap: 0.1em; }
.capability-toggle__name { font-size: var(--text-sm); color: var(--text-primary); }
.capability-toggle__desc { font-size: var(--text-xs); color: var(--text-secondary); }

/* [TARGETED: 2026-07-19] PWA INSTALL PROMPT — a non-blocking bottom-sheet
   (researched least-intrusive pattern). Backdrop + sheet slide up from the
   bottom. Logical properties (RTL-safe), tokens/em only, theme-aware,
   reduced-motion guarded. role=dialog wiring is in pwa.js. */
.pwa-install { position: fixed; inset: 0; z-index: 100; }
.pwa-install[hidden] { display: none; }
.pwa-install__backdrop {
  position: absolute; inset: 0;
  background: rgba(0, 0, 0, 0.55);         /* fallback for engines without oklch */
  background: var(--overlay-scrim);        /* modern: theme-independent scrim token */
  opacity: 0; transition: opacity var(--duration-med, 0.2s) var(--ease-standard, ease-out);
}
.pwa-install.is-open .pwa-install__backdrop { opacity: 1; }
.pwa-install__sheet {
  position: absolute; inset-inline: 0; inset-block-end: 0;
  max-width: 30em; margin-inline: auto;
  background: var(--bg-panel); color: var(--text-primary);
  border: 0.0625em solid var(--border-default);
  /* 1.75em top corners = Material 3's 28dp bottom-sheet radius (primary-sourced),
     logical so they are the TOP corners in LTR and mirror in RTL. */
  border-start-start-radius: 1.75em; border-start-end-radius: 1.75em;
  /* pad-top leaves room for the grabber; safe-area-inset-bottom now works because
     the viewport meta carries viewport-fit=cover. */
  padding: var(--space-xl) var(--space-lg) calc(var(--space-lg) + env(safe-area-inset-bottom, 0px));
  box-shadow: var(--shadow-lg);
  transform: translateY(100%);
  transition: transform var(--duration-med, 0.2s) var(--ease-standard, ease-out);
}
.pwa-install.is-open .pwa-install__sheet { transform: translateY(0); }
/* [TARGETED: 2026-07-19] Grabber handle — the sheet slides up, so a drag
   affordance matches Material 3 (32×4dp ≈ 2em×0.25em pill) + Apple HIG sheet
   convention. Purely decorative (aria-hidden via ::before). Muted natural grey. */
.pwa-install__sheet::before {
  content: ''; position: absolute; inset-block-start: 0.6em; inset-inline: 0;
  width: 2.25em; height: 0.25em; margin-inline: auto;
  border-radius: 999px; background: var(--border-medium, var(--border-default));
}
.pwa-install__x {
  position: absolute; inset-block-start: var(--space-sm); inset-inline-end: var(--space-sm);
  width: 2em; height: 2em; display: inline-flex; align-items: center; justify-content: center;
  border: none; background: none; color: var(--text-secondary); cursor: pointer; border-radius: 0.4em;
  transition: background var(--duration-fast, 0.15s) var(--ease-standard, ease-out);
}
.pwa-install__x:hover { background: var(--muted); color: var(--text-primary); }
.pwa-install__x .icon { width: 1.1em; height: 1.1em; }
.pwa-install__head { display: flex; align-items: center; gap: var(--space-md); margin-inline-end: 2em; }
.pwa-install__icon {
  width: 3em; height: 3em; flex-shrink: 0; border-radius: 0.6em;
  border: 0.0625em solid var(--border-default);
}
.pwa-install__title { margin: 0; font-size: var(--text-lg); }
.pwa-install__body { margin: var(--space-md) 0 var(--space-lg); font-size: var(--text-sm); color: var(--text-secondary); line-height: 1.5; }
.pwa-install__actions { display: flex; gap: var(--space-sm); }
.pwa-install__go { flex: 1; justify-content: center; }
.pwa-install__later { flex-shrink: 0; }

/* iOS step list */
.pwa-install__steps { list-style: none; margin: var(--space-md) 0 var(--space-lg); padding: 0; display: flex; flex-direction: column; gap: var(--space-md); }
.pwa-install__step { display: flex; align-items: center; gap: var(--space-sm); font-size: var(--text-sm); }
.pwa-install__step-icon, .pwa-install__step-num {
  flex-shrink: 0; width: 1.9em; height: 1.9em; display: inline-flex; align-items: center; justify-content: center;
  border-radius: 0.5em; background: var(--muted); color: var(--primary); font-weight: 600; font-size: var(--text-sm);
}
.pwa-install__share { width: 1.15em; height: 1.15em; }  /* the Share (square+up-arrow) glyph — vertical, no RTL flip needed */

@media (prefers-reduced-motion: reduce) {
  .pwa-install__backdrop, .pwa-install__sheet, .pwa-install__x { transition: none; }
  .pwa-install__sheet { transform: none; }   /* appear in place, no slide */
}

/* ===========================================================================
   [TARGETED: 2026-07-23] MARKETPLACE HOMEPAGE
   Hero slider + left facet column (desktop fixed / mobile drawer) + category
   cards + featured-listing feed. Reuses the design-system tokens and the
   .checkbox / .module-card / .empty-state / .pill components — nothing new is
   invented that an existing component already covers. Desktop and mobile are
   handled with SEPARATE rules (mobile-first base, then a min-width layer).
   All sizes in em; natural colours; full borders; subtle hover transitions.
   =========================================================================== */

/* The marketplace main gets a wider max-width than the default public-main so the
   two-column layout breathes on large screens. */
.mkt-main { max-width: 84em; }
.mkt-hero { margin-bottom: var(--space-xs); }

/* --- HERO SLIDER (modelled on .hero-gallery, its own component here) --- */
.mkt-slider-region { margin: 0; }

.mkt-slider {
  position: relative;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--bg-page);
  border: 1px solid var(--border-default);
}
.mkt-slider--single { line-height: 0; }

.mkt-slider__track {
  display: flex;
  margin: 0;
  padding: 0;
  list-style: none;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  scrollbar-width: none;
}
.mkt-slider__track::-webkit-scrollbar { display: none; }
@media (prefers-reduced-motion: reduce) { .mkt-slider__track { scroll-behavior: auto; } }

.mkt-slider__slide {
  position: relative;
  flex: 0 0 100%;
  scroll-snap-align: start;
  scroll-snap-stop: always;
  /* ART DIRECTION without a second image or any decode library: the SAME photo
     is framed differently per device by changing the box ratio (and, below, the
     crop focal point via object-position). A phone gets a taller box so a wide
     landscape photo still reads as a hero and the subject is not a thin strip;
     a tablet steps to a shallower ratio; desktop (min-width layer) becomes a
     wide banner. This is the 2025-recommended CSS route to per-device rendering
     when true server-side re-crops are not worth a native image dependency. */
  aspect-ratio: 16 / 11;
  margin: 0;
}
.mkt-slider__slide > img,
.mkt-slider__link > img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Keep the subject region visible when a wide photo is cropped tall on small
     screens — most metalworking hero shots put the subject slightly above centre. */
  object-position: center 42%;
}
/* Tablet: shallower box, subject re-centred as more width returns. */
@media (min-width: 30em) {
  .mkt-slider__slide { aspect-ratio: 21 / 10; }
  .mkt-slider__slide > img,
  .mkt-slider__link > img { object-position: center center; }
}
.mkt-slider__link { display: block; width: 100%; height: 100%; }
.mkt-slider__link:hover { opacity: 1; }

/* Caption overlaid on the slide, readable over any photo via a soft gradient. */
.mkt-slider__caption {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  padding: var(--space-lg) var(--space-lg) var(--space-md);
  background: linear-gradient(to top, rgba(0, 0, 0, 0.62), rgba(0, 0, 0, 0));
  color: var(--on-strong);
}
.mkt-slider__caption > span {
  font-size: var(--text-md);
  font-weight: 600;
  line-height: 1.25;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}

/* Prev/next — same lifted-button language as .hero-gallery__nav. */
.mkt-slider__nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Mobile-first: a 2.75em target clears the ~44px minimum touch size the
     research calls for; the desktop layer trims it back to 2.25em. */
  width: 2.75em;
  height: 2.75em;
  padding: 0;
  border: 1px solid var(--border-default);
  border-radius: 50%;
  background: color-mix(in srgb, var(--bg-panel) 88%, transparent);
  color: var(--text-primary);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-standard),
              box-shadow var(--duration-fast) var(--ease-standard),
              transform var(--duration-fast) var(--ease-standard);
}
.mkt-slider__nav:hover { background: var(--bg-panel); box-shadow: var(--shadow-md); }
.mkt-slider__nav:active { transform: translateY(-50%) scale(0.94); }
.mkt-slider__nav:focus-visible { outline: 0.125em solid var(--primary); outline-offset: 0.125em; }
.mkt-slider__nav > .icon { width: 1.1em; height: 1.1em; }
.mkt-slider__nav--prev { inset-inline-start: var(--space-md); }
.mkt-slider__nav--next { inset-inline-end: var(--space-md); }

.mkt-slider__dots {
  position: absolute;
  inset-inline: 0;
  bottom: var(--space-sm);
  display: flex;
  justify-content: center;
  gap: var(--space-xs);
}
.mkt-slider__dot {
  width: 0.5em;
  height: 0.5em;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: color-mix(in srgb, var(--on-strong) 55%, transparent);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-standard),
              transform var(--duration-fast) var(--ease-standard);
}
.mkt-slider__dot:hover { background: var(--on-strong); }
.mkt-slider__dot.is-active { background: var(--on-strong); transform: scale(1.35); }
.mkt-slider__dot:focus-visible { outline: 0.125em solid var(--primary); outline-offset: 0.125em; }

/* --- LAYOUT: left column + main area ---------------------------------- */
/* Mobile-first: single column, the facet column becomes a drawer (below). */
.mkt-layout {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-lg);
  align-items: start;
}

/* The mobile filter bar (trigger + count). Hidden on desktop. */
.mkt-filterbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
}
.mkt-filterbar[hidden] { display: none; }
.mkt-filterbar__toggle { display: inline-flex; align-items: center; gap: var(--space-xs); }
.mkt-filterbar__toggle > .icon { width: 1.1em; height: 1.1em; }
.mkt-filterbar__count { font-size: var(--text-sm); color: var(--text-secondary); }

/* --- FACET COLUMN ----------------------------------------------------- */
/* Mobile default: off-canvas drawer, revealed by [data-open="true"]. */
.mkt-facets {
  position: fixed;
  inset-block: 0;
  inset-inline-start: 0;
  z-index: 40;
  width: min(20em, 86vw);
  max-width: 86vw;
  background: var(--bg-panel);
  border-inline-end: 1px solid var(--border-default);
  box-shadow: var(--shadow-lg);
  padding: var(--space-lg);
  overflow-y: auto;
  transform: translateX(-102%);
  transition: transform var(--duration-med) var(--ease-standard);
}
:root[dir="rtl"] .mkt-facets,
[dir="rtl"] .mkt-facets { transform: translateX(102%); }
.mkt-facets[data-open="true"] { transform: translateX(0); }

.mkt-facets__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  margin-bottom: var(--space-md);
  padding-bottom: var(--space-sm);
  border-bottom: 1px solid var(--border-light);
}
.mkt-facets__title { margin: 0; font-size: var(--text-md); }
.mkt-facets__close[hidden] { display: none; }

.mkt-facets__form { display: flex; flex-direction: column; gap: var(--space-lg); }

.mkt-scrim {
  position: fixed;
  inset: 0;
  z-index: 39;
  background: var(--overlay-scrim);
  border: none;
}
.mkt-scrim[hidden] { display: none; }

/* One facet group. */
.mkt-facet {
  border: 1px solid var(--border-light);
  border-radius: var(--radius);
  padding: var(--space-md);
  margin: 0;
}
.mkt-facet__legend {
  padding: 0 var(--space-xs);
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
}
.mkt-facet__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-xs); }
.mkt-facet__item { margin: 0; }
.mkt-facet__label {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  width: 100%;
  padding: var(--space-xs) var(--space-xs);
  border-radius: 0.375em;
  transition: background-color var(--duration-fast) var(--ease-standard);
}
.mkt-facet__label:hover { background: var(--muted); }
.mkt-facet__name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.mkt-facet__count {
  font-size: var(--text-xs);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
  background: var(--muted);
  border-radius: 1em;
  padding: 0 0.5em;
}
.mkt-facets__actions { display: flex; justify-content: flex-start; }
.mkt-facets__clear[hidden] { display: none; }

/* --- MAIN CONTENT (right) --------------------------------------------- */
.mkt-content { display: flex; flex-direction: column; gap: var(--space-xl); min-width: 0; }
.mkt-section { display: flex; flex-direction: column; gap: var(--space-md); }
.mkt-section__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  flex-wrap: wrap;
  padding-bottom: var(--space-sm);
  border-bottom: 1px solid var(--border-light);
}
.mkt-section__title { margin: 0; font-size: var(--text-lg); }
.mkt-listings__count { font-variant-numeric: tabular-nums; }
.mkt-listings__empty[hidden] { display: none; }

/* A listing card whose <li> is hidden by the client filter must not leave a gap
   in the grid — [hidden] already collapses it, but be explicit. */
.mkt-listing-cards > li[hidden] { display: none; }

/* --- DESKTOP LAYER: left column becomes a real, always-visible sidebar --- */
@media (min-width: 60em) {
  .mkt-layout {
    grid-template-columns: 18em 1fr;
    gap: var(--space-xl);
  }
  /* The mobile trigger + scrim + close button are irrelevant on desktop. */
  .mkt-filterbar,
  .mkt-scrim { display: none !important; }
  .mkt-facets__close { display: none !important; }

  .mkt-facets {
    position: sticky;
    top: var(--space-lg);
    inset-inline-start: auto;
    inset-block: auto;
    z-index: auto;
    width: auto;
    max-width: none;
    max-height: calc(100vh - 2 * var(--space-lg));
    transform: none;
    box-shadow: none;
    border: 1px solid var(--border-default);
    border-radius: var(--radius);
  }
  :root[dir="rtl"] .mkt-facets,
  [dir="rtl"] .mkt-facets { transform: none; }
}

/* --- WIDE HERO on desktop: a strip, not a square. --- */
@media (min-width: 48em) {
  .mkt-slider__slide { aspect-ratio: 24 / 8; }
  /* Desktop has a mouse — the oversized touch target is no longer needed. */
  .mkt-slider__nav { width: 2.25em; height: 2.25em; }
}
