/* design.css — the shared token + component layer for every HTML binary
   (desk, owner, guest, staff), served from internal/webui/static.

   This file used to be physically duplicated between cmd/desk and cmd/owner,
   with "keep them in sync by hand" in its header. They were not in sync: the
   two copies had drifted by roughly 800 lines, and every fix had to be
   written twice. It now lives in one place and is served to all four
   binaries by internal/webui, whose Handler merges a shared asset layer under
   each app's own /static tree.

   What belongs HERE: design tokens, typography, and components more than one
   app renders — panels, cards, tables (.table-cards, .table-modern), status
   pills, list rows, form rows, the bottom tab bar, the sidebar shell.
   What does NOT: anything only one app draws. Desk's dashboard widgets,
   cleaning calendar, rail navigation and property tabs live in
   cmd/desk/static/desk.css; the owner portal's property tiles stay in
   cmd/owner/static/owner.css.

   Loaded ALONGSIDE each app's own stylesheet, not as a replacement. */

:root {
  /* Pending/awaiting-action — the status this repo's existing .badge-off
     was being asked to mean ("en attente") without actually looking like
     it (gray reads as "disabled", not "needs action"). New, not a rename:
     .badge-off keeps its existing gray/neutral meaning elsewhere (feature
     flags, "non implémenté" labels) — only .status-pill below uses amber
     for pending. */
  --pending: #b5540a;
  --pending-bg: #fdead9;

  /* staff.css's --s- tokens, unified into this namespace instead of a
     parallel one. Where a staff.css token already matches an app.css token
     in meaning, no new variable is introduced (e.g. --s-alert === --error,
     --s-accent === --brand for admin-side use — staff.css itself keeps its
     own warm palette deliberately, see its file header comment, this is
     only about not re-inventing names for anything reused outside it). */

  /* Spacing/radius/type/shadow scale — additive, introduced to stop new
     rules from hardcoding their own one-off px/rem values (every existing
     rule in app.css/design.css keeps doing that; this scale is for rules
     written from here on, adopted opportunistically as pages are touched,
     not a mass find-replace). Values chosen to match what's already in use
     the most, not invented from scratch. */
  --space-1: 0.25rem;  /* 4px */
  --space-2: 0.5rem;   /* 8px */
  --space-3: 0.75rem;  /* 12px */
  --space-4: 1rem;     /* 16px */
  --space-5: 1.5rem;   /* 24px */
  --space-6: 2rem;     /* 32px */
  --space-7: 3rem;     /* 48px */

  --radius-sm: 6px;   /* inputs, small controls — matches app.css's existing input radius */
  --radius-md: 10px;  /* cards, panels, action tiles — the existing default, unchanged */
  --radius-lg: 16px;  /* opt-in, for a softer/larger card treatment (see .card-elevated) */

  --text-xs: 0.75rem;
  --text-sm: 0.85rem;
  --text-base: 0.92rem;
  --text-lg: 1.15rem;   /* matches app.css's h2 */
  --text-xl: 1.6rem;    /* matches app.css's h1 */
  --text-2xl: 2rem;

  /* Soft, brand-tinted shadows (not pure black) — an opt-in alternative to
     the border-only depth cue everything else uses; see .card-elevated. */
  --shadow-sm: 0 1px 3px rgba(31, 78, 95, 0.08), 0 1px 2px rgba(31, 78, 95, 0.06);
  --shadow-md: 0 4px 10px rgba(31, 78, 95, 0.12), 0 2px 4px rgba(31, 78, 95, 0.08);
}

/* ============================================================================
   Card elevation — soft shadow + lighter border is now app.css's default
   .card/.panel treatment (see the app-wide retint block below). This class
   used to be the opt-in trial (/overview only); now every card already has
   the shadow/radius, so it's left as just the hover-lift micro-interaction
   for clickable card tiles — keep applying it alongside .card-link.
   ============================================================================ */
.card-elevated {
  transition: box-shadow 0.15s ease, transform 0.15s ease;
}
.card-elevated.card-link:hover {
  box-shadow: var(--shadow-md);
  transform: translateY(-2px);
  border-color: transparent;
}

/* ============================================================================
   Status pill — ONE component, ONE meaning-to-color mapping, for anything
   with a lifecycle status (devis, invoice, Stripe connection, sync run,
   email send). Distinct from the older .badge-* classes, which stay in use
   for non-status labels (feature flags, counts) — do not conflate the two.
   ============================================================================ */
.status-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.2rem 0.65rem;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 600;
  white-space: nowrap;
}
.status-pill-ok { background: var(--ok-bg); color: var(--ok); }
.status-pill-pending { background: var(--pending-bg); color: var(--pending); }
.status-pill-error { background: var(--error-bg); color: var(--error); }

/* A pill whose value was invented for a demonstration — the desk shows what
   Pennylane WOULD say while Pennylane is not connected (see
   loadPennylaneFreshness). Hatched rather than merely faded: a lighter pill
   reads as "less important", and this one has to read as "not real". The
   marking is per pill and not only per page, so a screenshot of one cell
   cannot lose the banner that framed it. */
.status-pill.is-simulated,
.badge.is-simulated {
  background-image: repeating-linear-gradient(
    -45deg, transparent 0 4px, rgba(0, 0, 0, 0.10) 4px 8px);
  opacity: 0.85;
  font-style: italic;
}

/* ============================================================================
   Action tile — staff.css's .staff-menu-card pattern, generalized: a
   full-width, large tap-target row that IS a link to do something (as
   opposed to .card/.card-value, which stays for pure metrics you don't tap
   to act on). Icon + label + optional sublabel + optional trailing badge.
   ============================================================================ */
.action-tile {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  width: 100%;
  padding: 0.85rem 1rem;
  min-height: 64px;
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  color: var(--text);
  text-decoration: none;
  transition: background-color 0.12s ease, border-color 0.12s ease;
}
.action-tile:hover,
.action-tile:active {
  text-decoration: none;
  background: var(--brand-light);
  border-color: var(--brand);
}
.action-tile-icon {
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--brand-light);
  color: var(--brand);
  font-size: 1.1rem;
}
.action-tile-icon.alert { background: var(--error-bg); color: var(--error); }
.action-tile-icon.pending { background: var(--pending-bg); color: var(--pending); }
.action-tile-text {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}
.action-tile-label { font-size: 1rem; font-weight: 700; }
.action-tile-sub { font-size: 0.85rem; color: var(--muted); }
.action-tile-badge {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.6rem;
  height: 1.6rem;
  padding: 0 0.4rem;
  border-radius: 999px;
  background: var(--error);
  color: #fff;
  font-size: 0.8rem;
  font-weight: 700;
}
.action-tile-badge.pending { background: var(--pending); }
.action-tile-badge.ok { background: var(--ok); }

.action-tile-list {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  margin-bottom: var(--space-6, 2rem);
}

.action-tile-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 0.6rem;
}

/* ============================================================================
   List row — a stacked-content row for mobile, standing in for a dense
   table row. Meant to be paired with a real <table> that's hidden on
   mobile / shown on desktop via the media query below, OR used as its own
   standalone list when there's no corresponding desktop table. Structure:
   <div class="list-row"><div class="list-row-main">title<span class="list-row-meta">meta</span></div><status-pill/></div>
   ============================================================================ */
.list-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.8rem 0.9rem;
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  text-decoration: none;
  color: var(--text);
}
.list-row:hover { text-decoration: none; background: var(--brand-light); }
.list-row-main {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.list-row-title { font-weight: 700; font-size: 0.95rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.list-row-meta { font-size: 0.82rem; color: var(--muted); }
.list-row-trailing { flex: none; display: flex; align-items: center; gap: 0.5rem; }

.list-rows { display: flex; flex-direction: column; gap: 0.5rem; }

/* A table that should present as list-rows on mobile: wrap it in
   <div class="table-as-list"> ... existing .table-wrap table ... </div>
   and give each <tr> a data-title/data-meta pair via template logic, OR
   (simpler, used first) just render .list-rows on mobile and the existing
   table on desktop from the same template with two blocks — see the
   redesign plan's per-page notes for which pages use which approach. */
@media (max-width: 640px) {
  .table-as-list .table-wrap { display: none; }
}
@media (min-width: 641px) {
  .list-rows-mobile-only { display: none; }
}

/* ============================================================================
   Modern table — opt-in alternative to app.css's boxed/grid table (border on
   every cell, solid-fill header, zebra striping). That style still suits
   genuinely spreadsheet-like grids (rates_recap's rr-grid, dense numeric
   tables meant to be scanned cell-by-cell); this one is for record lists
   read row-by-row: horizontal hairlines only, a quiet header (no fill,
   uppercase muted label), no zebra (rest state stays calm, hover carries
   the feedback instead), roomier row padding, and a sticky header so long
   lists (300-row cap) stay orientable while scrolling. Add alongside the
   existing table class, e.g. <table class="bookings-table table-modern">.
   ============================================================================ */
.table-modern { border-collapse: collapse; }
.table-modern th, .table-modern td { border: none; border-bottom: 1px solid var(--db-line-soft, var(--border)); }
.table-modern thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--panel-bg);
  color: var(--muted);
  font-size: 0.72rem;
  border-bottom: 2px solid var(--border);
}
.table-modern tbody tr:nth-child(even) td { background: transparent; }
.table-modern tbody tr { transition: background-color 0.12s ease; }
.table-modern tbody tr:hover td { background: var(--brand-light); }
/* rowlink.js — a row with data-href reads as clickable everywhere except
   its own nested links/buttons/inputs, which keep the normal pointer via
   their own element defaults. */
tr[data-href] { cursor: pointer; }
.table-modern td { padding: 0.85rem 0.9rem; }
.table-modern tbody tr:last-child td { border-bottom: none; }

/* A totals row, for the tables that carry one. Same treatment booktabs gives
   its own: a rule above, and weight — so the sum reads as the answer to the
   column rather than as one more line of it. */
.table-modern tfoot td {
  border-top: 1px solid var(--border);
  border-bottom: none;
  padding: 0.85rem 0.9rem;
  font-weight: 700;
}

/* ============================================================================
   Filter pills — a horizontally scrollable row of status filters, replacing
   a <select> where there are only a handful of common filter values worth
   one-tap access to on mobile (desktop gets the same row, just doesn't need
   to scroll it since there's more width).
   ============================================================================ */
.filter-pills {
  display: flex;
  gap: 0.5rem;
  overflow-x: auto;
  padding-bottom: 0.25rem;
  margin-bottom: 1rem;
  -webkit-overflow-scrolling: touch;
}
.filter-pill {
  flex: none;
  padding: 0.35rem 0.9rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--panel-bg);
  color: var(--text);
  font-size: 0.85rem;
  font-weight: 600;
  white-space: nowrap;
  text-decoration: none;
}
.filter-pill:hover { text-decoration: none; background: var(--brand-light); }
.filter-pill.active { background: var(--brand); border-color: var(--brand); color: #fff; }

/* ============================================================================
   Property tile grid — the owner portal's multi-property Accueil grid
   (cmd/owner only, but kept in the shared design.css for consistency should
   the admin app ever want the same pattern).
   ============================================================================ */
.property-tile-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 1rem;
  margin: 1rem 0 1.5rem;
}
.property-tile {
  display: block;
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
  background: var(--panel-bg);
  text-decoration: none;
  color: var(--text);
}
.property-tile:hover { text-decoration: none; border-color: var(--brand); }
.property-tile-thumb {
  display: block;
  width: 100%;
  height: 130px;
  background-size: cover;
  background-position: center;
  background-color: var(--brand-light);
}
.property-tile-noimg {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--brand);
  font-size: 1.8rem;
}
.property-tile-body {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  padding: 0.75rem 0.9rem;
}

/* ============================================================================
   Bottom tab bar (mobile navigation) — see nav.html. Desktop keeps the
   existing .sidebar from app.css; this and the sidebar are mutually
   exclusive via the media queries below, wired up in Phase 2.
   ============================================================================ */
.tab-bar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 100;
  display: flex;
  background: var(--panel-bg);
  border-top: 1px solid var(--border);
  padding-bottom: env(safe-area-inset-bottom, 0);
  box-shadow: 0 -2px 8px rgba(0,0,0,0.06);
}
.tab-bar-item {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.15rem;
  padding: 0.5rem 0.25rem 0.4rem;
  min-height: 56px;
  color: var(--muted);
  text-decoration: none;
  font-size: 0.72rem;
  font-weight: 600;
  position: relative;
}
.tab-bar-item:hover { text-decoration: none; }
.tab-bar-item i { font-size: 1.15rem; }
.tab-bar-item.active { color: var(--brand); }
.tab-bar-item-badge {
  position: absolute;
  top: 0.25rem;
  right: calc(50% - 1.1rem);
  min-width: 1.1rem;
  height: 1.1rem;
  padding: 0 0.25rem;
  border-radius: 999px;
  background: var(--error);
  color: #fff;
  font-size: 0.62rem;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}

@media (min-width: 900px) {
  .tab-bar { display: none; }
}
@media (max-width: 899px) {
  /* Room for the fixed bottom tab bar so page content isn't hidden behind
     it — applied to .content (app.css) in Phase 2 alongside the nav.html
     restructure, not here, since design.css must not change existing
     page layout on its own (Phase 1 is additive-only, see file header). */

  /* The section switcher sticks to the top.
   *
   * On a phone this strip IS the page's menu: tabs.js collapses it into a
   * single control naming the current section and opening a sheet listing the
   * others. It scrolled away with the content, so on a long record — a
   * booking has seven sections, a gîte more — switching section meant
   * scrolling all the way back up first. The bottom bar has always been
   * fixed; this one was not, and it is the only one of the two that talks
   * about the page being read.
   *
   * The negative margins cancel .content's padding: without them you watch
   * content scroll through the two gutters either side of the strip, which
   * reads as a rendering fault. */
  .tabs {
    position: sticky;
    top: 0;
    z-index: 30;
    background: var(--bg);
    margin-left: -1rem;
    margin-right: -1rem;
    padding: 0.5rem 1rem;
  }
}

/* ============================================================================
   Disclosure ("Aperçu" collapsed-by-default section on the admin Accueil,
   see Phase 3) — plain <details>/<summary> styling, no JS needed.
   ============================================================================ */
.disclosure > summary {
  cursor: pointer;
  font-weight: 700;
  font-size: 1.15rem;
  color: var(--brand);
  padding: 0.5rem 0;
  list-style: none;
}
.disclosure[open] > summary { margin-bottom: 0.75rem; }
.disclosure > summary::-webkit-details-marker { display: none; }
.disclosure > summary::before {
  content: "▸";
  display: inline-block;
  margin-right: 0.4rem;
  transition: transform 0.15s ease;
}
.disclosure[open] > summary::before { transform: rotate(90deg); }
@media (min-width: 900px) {
  /* Desktop: always show the Aperçu content, the disclosure chrome is a
     mobile-only affordance — Phase 3 opts desktop into this via the [open]
     attribute at render time rather than CSS forcing it, so "collapsed on
     mobile, open on desktop" doesn't fight a user who explicitly toggled it. */
}

/* ============================================================================
   Dashboard visual redesign — self-hosted display/body/mono type + a
   "Breton coast" palette (tide teal, warm stone, sand gold), scoped to the
   sidebar (shared chrome, restyled in place — nav.html is its only
   consumer) and to new .db-* components used by dashboard.html only. This
   deliberately does NOT touch app.css's global --bg/--brand/--panel-bg
   tokens or any other page's tables/forms/panels — see the redesign
   proposal this implements for the full rationale. Fonts vendored under
   static/vendor/fonts/, same self-hosting convention as Font Awesome.
   ============================================================================ */
@font-face {
  font-family: 'Fraunces Display';
  font-weight: 600;
  font-style: normal;
  font-display: swap;
  src: url(/static/vendor/fonts/fraunces-600.woff2?v=3a1de7711d14) format('woff2');
}
@font-face {
  font-family: 'Plex';
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  src: url(/static/vendor/fonts/plex-sans-400.woff2?v=3b646991d300) format('woff2');
}
@font-face {
  font-family: 'Plex';
  font-weight: 600;
  font-style: normal;
  font-display: swap;
  src: url(/static/vendor/fonts/plex-sans-600.woff2?v=8960851d691c) format('woff2');
}
@font-face {
  font-family: 'Plex';
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  src: url(/static/vendor/fonts/plex-sans-700.woff2?v=42e7b0c143c1) format('woff2');
}
@font-face {
  font-family: 'Plex Mono';
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  src: url(/static/vendor/fonts/plex-mono-500.woff2?v=01d285447409) format('woff2');
}
@font-face {
  font-family: 'Plex Mono';
  font-weight: 600;
  font-style: normal;
  font-display: swap;
  src: url(/static/vendor/fonts/plex-mono-600.woff2?v=0d1f0b8d0722) format('woff2');
}

:root {
  --db-stone:       #f1efe7;
  --db-stone-deep:  #e7e3d6;
  --db-surface:     #fffdf9;
  --db-surface-2:   #faf8f2;
  --db-ink:         #17232a;
  --db-ink-soft:    #52626a;
  --db-ink-faint:   #8b969b;
  --db-line:        #e2ddd0;
  --db-line-soft:   #ece8dc;

  --db-tide:        #1c4a58;
  --db-tide-deep:   #0e2b35;
  --db-tide-mid:    #2c6274;
  --db-tide-mist:   #e2edee;
  --db-tide-mist-2: #cfe1e3;

  --db-sand:        #b8823c;
  --db-sand-deep:   #8f6329;
  --db-sand-mist:   #f6ecd9;

  --db-ok:          #1f7a4d;
  --db-ok-mist:     #e2f2e8;
  --db-pending:     #b5540a;
  --db-pending-mist:#fbe9d7;
  --db-critical:    #ac2f26;
  --db-critical-mist:#f9e2df;

  --db-shadow-sm: 0 1px 2px rgba(15, 43, 53, 0.06), 0 1px 1px rgba(15, 43, 53, 0.04);
  --db-shadow-md: 0 8px 24px rgba(15, 43, 53, 0.10), 0 2px 6px rgba(15, 43, 53, 0.06);

  --db-font-display: 'Fraunces Display', Georgia, 'Iowan Old Style', serif;
  --db-font-body: 'Plex', -apple-system, 'Segoe UI', sans-serif;
  --db-font-mono: 'Plex Mono', ui-monospace, 'SFMono-Regular', Consolas, monospace;
}

/* ============================================================================
   App-wide retint — the dashboard redesign rolled out everywhere. Every
   shared component in app.css/design.css (.panel, .card, tables, buttons,
   .badge/.status-pill, forms, .notice, .action-tile, .list-row,
   .filter-pill, .tab-bar) is built from the CSS custom properties below,
   not hardcoded colors, so redefining them here — this block loads after
   both :root blocks above, same specificity, source order wins — retints
   the whole app in one place. --off/--off-bg stay untouched: that pair
   means "disabled/inactive", not a status, so it keeps its plain gray.
   ============================================================================ */
:root {
  --bg: var(--db-stone);
  --panel-bg: var(--db-surface);
  --border: var(--db-line);
  --text: var(--db-ink);
  --muted: var(--db-ink-soft);
  --brand: var(--db-tide);
  --brand-light: var(--db-tide-mist);

  --ok: var(--db-ok);
  --ok-bg: var(--db-ok-mist);
  --error: var(--db-critical);
  --error-bg: var(--db-critical-mist);
  --pending: var(--db-pending);
  --pending-bg: var(--db-pending-mist);

  --money: var(--db-sand-deep);
  --money-bg: var(--db-sand-mist);
  --people: #2f6f4f;
  --people-bg: #e7f2ec;
}

/* ---------------------------------------------------------------- rail */
/* Restyles the existing .sidebar (app.css) in place — same markup, same
   nav.html, only the visual treatment changes: deep tide teal instead of
   flat brand teal, a thin sand active-indicator instead of a filled hover
   pill, quieter section labels. Applies everywhere the sidebar renders
   (≥900px, same breakpoint app.css already uses), not just on the
   dashboard, since it's shared chrome. */
.sidebar {
  width: 248px;
  flex: 0 0 248px;
  background: var(--db-tide-deep);
  padding: 1.5rem 1.25rem;
  gap: 1.75rem;
  font-family: var(--db-font-body);
}
.sidebar .brand {
  font-family: var(--db-font-display);
  font-weight: 600;
  font-size: 1.15rem;
  gap: 0.6rem;
  /* .brand is now a link (to "/") — override the global a{color} rule so
     it stays white like the rest of the sidebar, with no hover underline. */
  color: #fff;
}
.sidebar .brand:hover { color: #fff; text-decoration: none; }
.sidebar .brand img { filter: brightness(0) invert(1); opacity: 0.95; }
.sidebar nav { gap: 1.4rem; }
.sidebar nav a {
  color: #c3d8db;
  font-size: 0.88rem;
  font-weight: 500;
  padding: 0.5rem 0.7rem;
  border-radius: 8px;
  border-left: 2px solid transparent;
  margin-left: -2px;
}
.sidebar nav a:hover { background: rgba(255,255,255,0.06); color: #fff; }
.sidebar nav a.active {
  background: rgba(255,255,255,0.09);
  color: #fff;
  border-left-color: var(--db-sand);
}
.sidebar nav a i { opacity: 0.85; }
.sidebar nav .nav-section {
  font-size: 0.68rem;
  letter-spacing: 0.09em;
  color: #6f95a0;
  margin-top: 1.4rem;
}
.nav-user {
  border-top: 1px solid rgba(255,255,255,0.08);
  padding-top: 1rem;
  font-size: 0.78rem;
  color: #7e9ea7;
  font-family: var(--db-font-body);
}
.nav-user .link-btn {
  background: none; border: none; color: #7e9ea7; padding: 0;
  cursor: pointer; font-size: 0.78rem; text-decoration: underline;
  text-underline-offset: 2px; font-family: inherit;
}
.nav-user .link-btn:hover { color: #fff; }

/* ============================================================================
   .table-cards — the general "table becomes one card per row on mobile"
   component. Generalizes the technique app.css already used twice for
   .property-table and .option-table, which stay as they are: both have a
   genuinely bespoke card layout (an absolutely-positioned thumbnail; a
   checkbox+label grid) that this generic version deliberately doesn't try
   to express.

   Chosen over the other responsive pattern in this repo (the dual render:
   a .list-rows.list-rows-mobile-only block next to a .table-as-list table,
   see quotes_list.html) as the DEFAULT for every remaining listing, because
   the dual render writes the same data twice in the template — every column
   change has to be made in two places, and any table containing a <form>
   would have its inputs duplicated, which breaks name= semantics outright.
   The dual render stays for the handful of wide lists whose mobile card
   genuinely needs a different information hierarchy, not just a narrower one.

   Usage: add `table-cards` alongside the table's other classes, give every
   <td> a data-label matching its column header, and mark the row's title
   column .cell-primary (rendered on its own line, no label). Columns not
   worth carrying onto a phone get .cell-hide-sm.

     <table class="bookings-table table-modern table-cards">
       <thead><tr><th>Client</th><th>Du</th><th class="amount">Montant</th></tr></thead>
       <tr>
         <td class="cell-primary" data-label="">Dupont</td>
         <td data-label="Du">2026-07-04</td>
         <td class="amount" data-label="Montant">840 €</td>
       </tr>
     </table>
   ============================================================================ */
@media (max-width: 640px) {
  .table-cards,
  .table-cards tbody,
  .table-cards td { display: block; width: 100%; }
  .table-cards thead { display: none; }

  /* The card is a flex column, not a plain block, so .cell-primary and
     .cell-actions can be pulled to the top and bottom of the card with
     `order` no matter where their column sits in the table. Without it,
     every template would have to put its title column first and its action
     column last to read correctly — a constraint on the DESKTOP column
     order, which is not ours to impose. */
  .table-cards tr {
    display: flex;
    flex-direction: column;
    width: 100%;
    border: 1px solid var(--border);
    border-radius: var(--radius-md, 10px);
    background: var(--panel-bg);
    box-shadow: var(--shadow-sm);
    padding: 0.7rem 0.9rem;
    margin-bottom: 0.7rem;
  }
  /* The desktop hover/zebra fills would paint over the card background. */
  .table-cards tbody tr:hover td,
  .table-cards tbody tr:nth-child(even) td { background: none; }
  .table-cards tbody tr:last-child td { border-bottom: none; }

  .table-cards td {
    border: none;
    padding: 0.28rem 0;
    display: flex;
    /* wrap: the value is an anonymous flex item, which CSS cannot target to
       give it `min-width: 0` — so without wrapping, a value longer than the
       space left by its label overflows the card instead of shrinking (a
       long property name on /reconciliation was the case that caught this).
       Allowed to wrap, it drops to its own full-width line and breaks
       normally. `anywhere` additionally breaks values with no space in them
       at all: IBANs, ids, URLs, env var names. */
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: baseline;
    gap: 0.15rem 0.9rem;
    /* left, not right: `justify-content: space-between` already pushes a
       one-line value to the card's right edge, so text-align only takes
       effect once the value has wrapped onto its own full-width line — and
       there, right-aligned prose reads as ragged and broken. .amount opts
       back in below, where right alignment is about the digits. */
    text-align: left;
    overflow-wrap: anywhere;
  }
  .table-cards td[data-label]:not([data-label=""])::before {
    content: attr(data-label);
    flex: 0 0 auto;
    text-align: left;
    color: var(--muted);
    font-family: var(--db-font-body, inherit);
    font-size: var(--text-xs, 0.75rem);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
  }

  /* Title column: full width, no label, reads as the card's heading. */
  .table-cards td.cell-primary {
    order: -1;
    display: block;
    text-align: left;
    overflow-wrap: anywhere;
    font-size: 1.02rem;
    font-weight: 600;
    padding: 0 0 0.45rem;
    margin-bottom: 0.35rem;
    border-bottom: 1px solid var(--border);
  }
  .table-cards td.cell-primary::before { display: none; }
  .table-cards td.cell-primary a { font-weight: 600; }

  /* Columns not worth the vertical space on a phone (technical ids, raw
     payloads, redundant timestamps). Kept in the DOM for desktop. */
  .table-cards td.cell-hide-sm { display: none; }

  /* A cell holding free text (a message excerpt, an error, an answer):
     the label sits above it and the text uses the full card width. */
  .table-cards td.cell-block {
    display: block;
    text-align: left;
  }
  .table-cards td.cell-block[data-label]:not([data-label=""])::before {
    display: block;
    margin-bottom: 0.15rem;
  }

  /* The trailing "actions" cell (a column with an empty <th>): its buttons
     and links sit on their own line at the foot of the card, separated by a
     hairline, rather than right-aligned against an invisible label. */
  .table-cards td.cell-actions {
    order: 1;
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-start;
    align-items: center;
    gap: 0.5rem;
    text-align: left;
    margin-top: 0.35rem;
    padding-top: 0.5rem;
    border-top: 1px solid var(--border);
  }
  .table-cards td.cell-actions:empty { display: none; }

  /* Rows whose cells hold form controls: inputs go full width under their
     label rather than being squeezed into the right-hand column. */
  .table-cards td.cell-form { display: block; text-align: left; }
  .table-cards td.cell-form[data-label]:not([data-label=""])::before {
    display: block;
    margin-bottom: 0.25rem;
  }
  .table-cards td.cell-form input:not([type="checkbox"]):not([type="radio"]),
  .table-cards td.cell-form select,
  .table-cards td.cell-form textarea { width: 100%; max-width: 100%; }
  .table-cards td.cell-form .inline-form { flex-wrap: wrap; }

  /* Amount cells keep their mono/tabular treatment but stop being
     right-aligned as a column — they're already right-aligned as the
     value half of a label/value pair. */
  .table-cards td.amount { white-space: nowrap; text-align: right; }

  /* An empty cell in a table is a blank box in a column that still has a
     header; on a card it would be a label with nothing after it. Note this
     only matches a td with NO content at all — a cell rendering an explicit
     "—" placeholder is not empty and still shows, which is the intent.
     Must come LAST in this block: .cell-block/.cell-form/.cell-actions all
     set `display` at the same specificity, so source order decides. */
  .table-cards td:empty { display: none; }
}

/* ============================================================================
   Scroll affordance for every remaining horizontally-scrolling table (the
   two real matrices, .cc-grid and .rr-grid, plus any .table-cards table
   above 640px that still overflows). Without it a table that continues off
   the right edge looks like a table that simply ends there. Pure CSS: the
   two linear-gradients are painted in the scroller's own coordinate space
   (background-attachment: local) so they scroll away once you reach an
   edge, while the two radial "shadows" are fixed to the viewport of the
   scroller (scroll) and stay visible — the classic Lea Verou technique.
   ============================================================================ */
.table-wrap {
  background:
    linear-gradient(90deg, var(--panel-bg) 30%, rgba(255, 253, 249, 0)) left center / 28px 100% no-repeat,
    linear-gradient(90deg, rgba(255, 253, 249, 0), var(--panel-bg) 70%) right center / 28px 100% no-repeat,
    radial-gradient(farthest-side at 0 50%, rgba(15, 43, 53, 0.14), rgba(15, 43, 53, 0)) left center / 12px 100% no-repeat,
    radial-gradient(farthest-side at 100% 50%, rgba(15, 43, 53, 0.14), rgba(15, 43, 53, 0)) right center / 12px 100% no-repeat;
  background-attachment: local, local, scroll, scroll;
}
/* Once the table has collapsed to cards there is nothing to scroll, and the
   gradients would paint stripes down the side of the card stack. */
@media (max-width: 640px) {
  .table-wrap:has(.table-cards) { background: none; }
}

/* ============================================================================
   .form-rows / .form-row — a list of editable records, each row being a form.
   Replaces the pattern (property_bedrooms.html, property_faqs.html) of a
   <table> whose every <tr> held a single <td colspan="N"> wrapping the form,
   under a <thead> whose column labels aligned with nothing. That markup was
   a table used as a border box: it gave no column semantics to a screen
   reader, and on a phone it inherited .table-wrap's horizontal scroll for a
   layout that never had columns to begin with.
   ============================================================================ */
.form-rows { display: flex; flex-direction: column; gap: 0.6rem; margin: 0.5rem 0; }
.form-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.6rem;
  padding: 0.8rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-md, 10px);
  background: var(--panel-bg);
}
.form-row .inline-form { margin: 0; flex-wrap: wrap; align-items: flex-end; gap: 0.6rem; }
.form-row label { display: flex; flex-direction: column; gap: 0.2rem; font-size: var(--text-xs, 0.75rem); color: var(--muted); font-weight: 600; }
@media (max-width: 640px) {
  .form-row { flex-direction: column; align-items: stretch; }
  .form-row .inline-form { flex-direction: column; align-items: stretch; }
  .form-row input:not([type="checkbox"]):not([type="radio"]),
  .form-row select,
  .form-row textarea,
  .form-row button { width: 100%; max-width: 100%; }
}

/* ============================================================================
   Tabs — ONE component for every "switch between sections of this thing"
   control in the app, replacing three that had drifted apart: .prop-tabs
   (the 16 sub-pages of a gîte, in desk.css, a horizontally scrolling
   underline strip), .tablist.seg (the ARIA tab sets on /customers/{id},
   /integrations and /settings/business, a segmented pill group) and, by
   visual accident, the .seg filter bars in table toolbars, which do a
   completely different job and looked exactly like the second.

   TWO CONTROLS, NOT ONE. Keeping them apart is the point:
     .tabs  — sections of a single object. Underlined, sitting on a rule, so
              it reads as "the thing above continues below in these parts".
     .seg   — a filter over one list ("À venir / Passées / Toutes"). Stays a
              pill group. A filter is not a section, and after this change
              the two no longer look alike.

   NO HORIZONTAL SCROLL, EVER. The strip used to scroll, which hid over half
   of a gîte's sections off the right edge with nothing to say they were
   there. Material reserves scrollable tabs for *browsing*, "when users don't
   need to directly compare the tab labels" — but reaching a gîte's Photos
   page is targeted navigation, not browsing, and NN/g's mobile findings are
   consistent that navigation which is visible gets used and navigation which
   is hidden does not. So: wrap on desktop, and on a phone collapse to the
   picker below.

   DESKTOP: an underlined strip that wraps onto a second row rather than
   scrolling. Every section is visible and clickable at all times.

   NO FILL ON THE BAR, and none on the resting active tab — the strip sits
   directly on the page background and the current section is marked by its
   underline and colour. A filled bar with a differently-filled active item
   is a segmented control, which is precisely the .seg language reserved
   above for filters: giving tabs that treatment would undo the one
   distinction this component exists to draw. It would also add a third
   surface tone between the page and the panels below it, carrying no
   information. The underline is itself a non-colour indicator, so the
   active tab never depends on hue alone.

   The sheet is the exception, deliberately: there a tab is a list row, and
   a row needs a fill plus a checkmark to read as chosen — an underline
   under one row of a vertical list says nothing.

   MOBILE: the strip collapses into a single full-width control showing the
   current section, which opens a bottom sheet listing every section — the
   native iOS/Android idiom for "this screen has more sections than fit", and
   the one that matches the app-style .tab-bar already fixed to the bottom of
   these pages. One tap to see everything, one to choose; nothing hidden off
   an edge.

   The switch between the two is MEASURED, not guessed (see tabs.js): the
   script collapses a set only when its strip actually overflows its
   container. No item-count threshold to tune, and it stays correct for a
   three-tab set with long French labels as well as a sixteen-tab one.

   Markup — same for a nav-tab set (links to other URLs) and an ARIA tab set
   (panels on one page); only the roles differ:

     <div class="tabs">
       <nav class="tabs-list" aria-label="Sections de ce gîte">
         <a class="tab is-active" aria-current="page">…</a>
       </nav>
     </div>

   tabs.js injects the picker button and the scrim; with JavaScript off the
   strip simply wraps to as many rows as it needs, which is a complete and
   usable fallback rather than a degraded one.
   ============================================================================ */
.tabs { position: relative; margin: 0 0 1.25rem; }

.tabs-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0 0.1rem;
  border-bottom: 1px solid var(--border);
}
.tab {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.55rem 0.8rem;
  /* Pulls the item's own underline onto the list's rule so the active tab
     overwrites it rather than drawing a second line beneath it. */
  margin-bottom: -1px;
  border: 0;
  border-bottom: 2px solid transparent;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: var(--text-sm, 0.85rem);
  font-weight: 600;
  line-height: 1.3;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
}
/* A fill on hover, not at rest: the strip itself stays on the page
   background (see the header's note on why it is not a filled bar), but the
   whole tab — not just its text — has to read as the target, because the
   padding around a short label like "FAQ" is most of its click area. */
.tab { border-radius: var(--radius-sm, 6px) var(--radius-sm, 6px) 0 0; }
.tab:hover { color: var(--brand); background: var(--hover-bg, #f5f7f8); text-decoration: none; }
.tab:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; border-radius: var(--radius-sm, 6px); }
.tab i { font-size: 0.85em; opacity: 0.9; }
/* Three ways of saying "this one": a link to the current page, a selected
   ARIA tab, and the class tabs.js toggles. All look identical on purpose. */
.tab.is-active,
.tab[aria-current="page"],
.tab[aria-selected="true"] { color: var(--brand); border-bottom-color: var(--brand); }
.tab-count {
  font-variant-numeric: tabular-nums;
  font-size: var(--text-xs, 0.75rem);
  opacity: 0.75;
}

/* --- Collapsed (phone) ---------------------------------------------------
   tabs.js adds .is-collapsed once it has measured that the strip overflows.
   Until then nothing here applies, so a set that fits keeps its tabs on
   screen — visible beats hidden whenever visible is affordable. */
.tabs-current { display: none; }

.tabs.is-collapsed .tabs-current {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  width: 100%;
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-md, 10px);
  background: var(--panel-bg);
  box-shadow: var(--shadow-sm);
  color: var(--brand);
  font: inherit;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}
.tabs.is-collapsed .tabs-current .tabs-current-label {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Caret drawn from two borders on a rotated box — no glyph needed (see
   tabs.js), and it can point up while the sheet is open, which is how a
   native picker says "tap again to dismiss". */
.tabs.is-collapsed .tabs-current .tabs-current-caret {
  flex: 0 0 auto;
  width: 0.45rem;
  height: 0.45rem;
  margin-right: 0.15rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  opacity: 0.55;
  transform: translateY(-0.1rem) rotate(45deg);
  transition: transform 0.2s ease;
}
.tabs.is-open .tabs-current .tabs-current-caret {
  transform: translateY(0.1rem) rotate(225deg);
}
@media (prefers-reduced-motion: reduce) {
  .tabs.is-collapsed .tabs-current .tabs-current-caret { transition: none; }
}

/* The sheet itself: the same <nav class="tabs-list"> element, re-laid-out
   and re-parented to <body> by tabs.js (which also adds .tabs-sheet). Same
   reasoning as the scrim — a full-viewport overlay must not depend on what
   happens to be above it in the tree, and here it is load-bearing: an
   ancestor that creates a stacking context traps the sheet's z-index inside
   it, and the body-level scrim then paints over the sheet instead of under
   it. Re-using the element rather than rendering a second copy of every
   link keeps one source of truth in the DOM, so nothing can drift between
   the two forms and assistive technology is never offered the list twice. */
.tabs-list.tabs-sheet {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  /* Above the scrim (1000), and both far above the bottom .tab-bar's 100,
     so neither the scrim nor the app's own chrome can surface through an
     open sheet. */
  z-index: 1001;
  display: block;
  max-height: 75vh;
  overflow-y: auto;
  padding: 0.5rem 0 calc(0.5rem + env(safe-area-inset-bottom, 0px));
  border: 0;
  border-radius: 16px 16px 0 0;
  background: var(--panel-bg);
  box-shadow: 0 -8px 30px rgba(0, 0, 0, 0.18);
  /* Off-screen and inert until opened. visibility (not display) so the
     transform can animate, and so nothing is focusable behind the scrim. */
  transform: translateY(100%);
  visibility: hidden;
  transition: transform 0.22s ease, visibility 0.22s;
}
.tabs-sheet.is-open { transform: none; visibility: visible; }

/* The grab handle every native sheet has — the affordance that says "this
   panel came up from the bottom and can go back down". */
.tabs-sheet::before {
  content: "";
  display: block;
  width: 2.25rem;
  height: 0.25rem;
  margin: 0.15rem auto 0.6rem;
  border-radius: 999px;
  background: var(--border);
}

/* Sheet heading — injected by tabs.js from the list's aria-label, hidden
   while the strip is a strip (where the label is already implied by the
   page it sits on). */
.tabs-sheet-title { display: none; }
.tabs-sheet .tabs-sheet-title {
  display: block;
  margin: 0 0 0.35rem;
  padding: 0 1.15rem 0.5rem;
  border-bottom: 1px solid var(--border);
  color: var(--muted);
  font-size: var(--text-xs, 0.75rem);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* Inside the sheet a tab is a full-width row, not a chip: touch target the
   width of the screen, icon in a fixed gutter so the labels align. */
.tabs-sheet .tab {
  display: flex;
  width: 100%;
  gap: 0.85rem;
  padding: 0.85rem 1.15rem;
  margin: 0;
  border: 0;
  border-radius: 0;
  font-size: var(--text-base, 0.92rem);
  white-space: normal;
}
.tabs-sheet .tab i:first-child {
  flex: 0 0 1.25rem;
  font-size: 1rem;
  text-align: center;
}
.tabs-sheet .tab.is-active,
.tabs-sheet .tab[aria-current="page"],
.tabs-sheet .tab[aria-selected="true"] { background: var(--brand-light, #e2edee); }
/* A checkmark, the way a native picker marks the current choice. */
.tabs-sheet .tab.is-active::after,
.tabs-sheet .tab[aria-current="page"]::after,
.tabs-sheet .tab[aria-selected="true"]::after {
  content: "\f00c";
  font-family: "Font Awesome 6 Free";
  font-weight: 900;
  margin-left: auto;
  font-size: 0.85rem;
}

/* Lives on <body>, not inside .tabs — see tabs.js for why — so it carries
   its own state class rather than inheriting one from a parent. */
.tabs-scrim {
  position: fixed;
  inset: 0;
  z-index: 1000;
  background: rgba(13, 27, 33, 0.42);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.22s ease, visibility 0.22s;
}
.tabs-scrim.is-open { opacity: 1; visibility: visible; }

@media (prefers-reduced-motion: reduce) {
  .tabs-sheet,
  .tabs-scrim { transition: none; }
}
