/* Token system adopted from the owner's design mock (Task 15, 2026-08-11 --
   .superpowers/sdd/2026-08-11-louer-renter-check/louer-design.html is the
   spec of record). Three layers, in rising specificity so the cascade
   resolves correctly regardless of source order:
     1. :root                                  -- light, the bare default
     2. @media (prefers-color-scheme: dark) :root -- dark, when no explicit
        choice has been made (first-visit default per the brief)
     3. html.dark / html.light                 -- explicit user override via
        the nav moon button (public/app.js), persisted to localStorage
        'lr:theme'. An element+class selector outranks a bare :root or a
        media-scoped :root on specificity alone, so these always win over
        the system default once either class is present -- no !important
        and no dependence on rule order needed. */
:root {
  --bg: #f0f1f3; --text: rgba(0,0,0,0.85); --text2: rgba(0,0,0,0.5); --text3: rgba(0,0,0,0.35);
  --glass: rgba(255,255,255,0.6); --glass-strong: rgba(244,245,247,0.88);
  --glass-border: rgba(255,255,255,0.35); --row-hover: rgba(255,255,255,0.55);
  --hairline: rgba(0,0,0,0.08); --chip: rgba(0,0,0,0.045);
  --link: #1e4f8f; --link-h: #163c6e; --accent: rgb(0,122,255); --bar-track: rgba(0,0,0,0.08);
  --card-solid: #ffffff; --map-bg: #dfe3e8;
  --good: #0ca30c; --warn: #b8790a; --bad: #d03b3b; --ring: #8b1a1a; --str: #2a78d6;
  --good-bg: rgba(12,163,12,0.10); --warn-bg: rgba(250,178,25,0.14); --bad-bg: rgba(208,59,59,0.10);
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #191b1f; --text: rgba(255,255,255,0.85); --text2: rgba(255,255,255,0.55); --text3: rgba(255,255,255,0.35);
    --glass: rgba(28,28,30,0.6); --glass-strong: rgba(28,29,33,0.88);
    --glass-border: rgba(255,255,255,0.10); --row-hover: rgba(255,255,255,0.06);
    --hairline: rgba(255,255,255,0.10); --chip: rgba(255,255,255,0.09);
    --link: #7db4f5; --link-h: #a8ccf8; --card-solid: #232529; --bar-track: rgba(255,255,255,0.10);
    --good: #5fd35f; --warn: #f3c14f; --bad: #ff6b6b; --map-bg: #14181f;
    --good-bg: rgba(95,211,95,0.12); --warn-bg: rgba(243,193,79,0.12); --bad-bg: rgba(255,107,107,0.12);
  }
}
html.dark {
  --bg: #191b1f; --text: rgba(255,255,255,0.85); --text2: rgba(255,255,255,0.55); --text3: rgba(255,255,255,0.35);
  --glass: rgba(28,28,30,0.6); --glass-strong: rgba(28,29,33,0.88);
  --glass-border: rgba(255,255,255,0.10); --row-hover: rgba(255,255,255,0.06);
  --hairline: rgba(255,255,255,0.10); --chip: rgba(255,255,255,0.09);
  --link: #7db4f5; --link-h: #a8ccf8; --card-solid: #232529; --bar-track: rgba(255,255,255,0.10);
  --good: #5fd35f; --warn: #f3c14f; --bad: #ff6b6b; --map-bg: #14181f;
  --good-bg: rgba(95,211,95,0.12); --warn-bg: rgba(243,193,79,0.12); --bad-bg: rgba(255,107,107,0.12);
}
html.light {
  --bg: #f0f1f3; --text: rgba(0,0,0,0.85); --text2: rgba(0,0,0,0.5); --text3: rgba(0,0,0,0.35);
  --glass: rgba(255,255,255,0.6); --glass-strong: rgba(244,245,247,0.88);
  --glass-border: rgba(255,255,255,0.35); --row-hover: rgba(255,255,255,0.55);
  --hairline: rgba(0,0,0,0.08); --chip: rgba(0,0,0,0.045);
  --link: #1e4f8f; --link-h: #163c6e; --bar-track: rgba(0,0,0,0.08);
  --card-solid: #ffffff; --map-bg: #dfe3e8;
  --good: #0ca30c; --warn: #b8790a; --bad: #d03b3b; --ring: #8b1a1a; --str: #2a78d6;
  --good-bg: rgba(12,163,12,0.10); --warn-bg: rgba(250,178,25,0.14); --bad-bg: rgba(208,59,59,0.10);
}
@media (prefers-color-scheme: dark) {
  /* Token-driven (var(--glass-strong)/var(--text)) instead of a hardcoded
     rgb() so html.dark/html.light track it too. */
  .leaflet-popup-content-wrapper, .leaflet-popup-tip { background: var(--glass-strong); color: var(--text); }
}
html.dark .leaflet-popup-content-wrapper, html.dark .leaflet-popup-tip { background: var(--glass-strong); color: var(--text); }
/* Dark map tile chrome (owner feedback, 2026-08-12): the invert/hue-rotate
   filter this block used to apply to .leaflet-tile is gone -- public/
   app.js now swaps in CARTO's real dark_all basemap instead (see initMap),
   which beats any CSS filter trick on an OSM raster. Attribution styling
   stays (non-tile chrome, still needs to read on a dark tile). :not(.light)
   on the media-query branch matters (unlike the popup rule above, which
   has no light-mode counterpart to conflict with): without it, an explicit
   light-mode choice on a dark-OS device would still darken the attribution
   pill even though the tiles underneath it are the light CARTO basemap. */
@media (prefers-color-scheme: dark) {
  html:not(.light) .leaflet-control-attribution { background: rgba(28,28,30,0.7); color: rgba(255,255,255,0.55); }
  html:not(.light) .leaflet-control-attribution a { color: rgba(255,255,255,0.75); }
}
html.dark .leaflet-control-attribution { background: rgba(28,28,30,0.7); color: rgba(255,255,255,0.55); }
html.dark .leaflet-control-attribution a { color: rgba(255,255,255,0.75); }
* { box-sizing: border-box; }
[hidden] { display: none !important; }
html, body { margin: 0; padding: 0; }
body { font: 16px/1.5 -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Roboto, sans-serif;
  color: var(--text); background: var(--bg); -webkit-font-smoothing: antialiased; }
main { max-width: 680px; margin: 0 auto; padding: 24px 14px 40px; display: flex; flex-direction: column; gap: 12px; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0,0,0,0); white-space: nowrap; border: 0; }

/* Full-screen map landing (owner pivot, 2026-08-11): / is the map, edge to
   edge, with every other page keeping the normal scrolling layout. Scoped
   entirely under body.page-home (set by layout.js) so nothing here can leak
   onto /proprietaire or /guide. */
body.page-home { height: 100dvh; overflow: hidden; }
body.page-home main { position: fixed; inset: 0; max-width: none; margin: 0; padding: 0;
  display: block; overflow: hidden; }
body.page-home footer { display: none; }
/* The header becomes a floating pill instead of a full-width sticky bar --
   position: fixed (not sticky) so it's pulled out of normal flow entirely
   and doesn't reserve height main would otherwise have to sit below. */
/* top stays a plain 14px, not env(safe-area-inset-top,...) -- public/app.js's
   --topbar-h (read by #sugg/#pick-hint/#geo-msg below to sit just under
   this pill) is measured from the header's own rendered height only, not
   its top offset, so adding a safe-area inset here without also folding it
   into every one of those would widen the gap on notched devices instead
   of closing it. Left as a known follow-up; see the task report. */
body.page-home .site-header { position: fixed; top: 14px; left: 14px; right: 14px; z-index: 30;
  width: auto; margin: 0 auto; max-width: calc(100vw - 28px);
  border-radius: 999px; justify-content: center; gap: 8px 14px;
  box-shadow: inset 0 0 0 1px var(--glass-border), 0 6px 20px rgba(0,0,0,0.18); }
@media (min-width: 641px) {
  /* The merged bar (brand + search + near-me + lang + theme, owner
     feedback round 2026-08-11) reads heavy at full viewport width once
     there's room to spare -- capped and centered, same width the old
     .floating-search used to claim on its own. */
  body.page-home .site-header { width: 560px; max-width: calc(100vw - 28px); }
}
a { color: var(--link); } a:hover { color: var(--link-h); }
select:focus, button:focus-visible, a:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(0,122,255,0.4); }
h1 { margin: 0 0 4px; font-size: 24px; font-weight: 700; letter-spacing: -0.022em; line-height: 1.15; text-wrap: pretty; }
h2 { font-size: 18px; font-weight: 700; letter-spacing: -0.015em; margin: 14px 0 6px; }

/* Single merged top bar (owner feedback round, 2026-08-11): brand, the
   home-only search input + near-me button (headerSearch, interpolated by
   src/render/layout.js -- empty on every other page), language switch, and
   the theme toggle all sit in this one glass bar. No more separate
   Vérifier/Propriétaire/Le Jeu nav links -- see .sheet-sidebar below for
   where that navigation lives now. */
.site-header { position: sticky; top: 0; z-index: 1000; background: var(--glass-strong);
  backdrop-filter: saturate(180%) blur(18px); -webkit-backdrop-filter: saturate(180%) blur(18px);
  box-shadow: inset 0 0 0 1px var(--glass-border), 0 0.5px 0 var(--hairline);
  display: flex; align-items: center; gap: 10px;
  padding: 8px 8px 8px 16px; flex-wrap: wrap; }
.brand { display: flex; align-items: center; gap: 8px; text-decoration: none; color: var(--text);
  font-size: 15px; font-weight: 600; letter-spacing: -0.01em; flex-shrink: 0; }
/* Decorative wordmark dot (Task 15, per the mock) -- fixed --bad tint, not
   tied to any live data; purely a brand accent, same as the mock's own. */
.brand-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--bad); box-shadow: 0 0 0 2.5px var(--bad-bg); flex-shrink: 0; }
/* The "Louer" wordmark used to hide below 420px to free up room for the
   search input (owner's original brief). Dogfood pass (2026-08-12) flagged
   this as a trust problem -- a bare dot with no name reads as an ad/tracker
   on a first visit -- so the wordmark now always stays on screen; the
   header-search flex rules below already give the input room without it. */
.header-search { display: flex; align-items: center; gap: 6px; flex: 1 1 auto; min-width: 0; }
.header-search .search-input { flex: 1 1 auto; min-width: 0; height: 34px; }
.header-search .btn { flex-shrink: 0; }
.header-utils { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.lang-switch { font-size: 12px !important; font-weight: 600 !important; padding: 3px 9px;
  border-radius: 999px; background: var(--chip); box-shadow: inset 0 0 0 1px var(--glass-border); }
.theme-btn { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px;
  border-radius: 999px; background: var(--chip); box-shadow: inset 0 0 0 1px var(--glass-border);
  border: none; color: var(--text2); cursor: pointer; padding: 0; flex-shrink: 0; }
.theme-btn:hover { color: var(--text); }
.theme-btn svg { display: block; width: 13px; height: 13px; }

.glass-card { background: var(--glass); backdrop-filter: saturate(180%) blur(40px);
  -webkit-backdrop-filter: saturate(180%) blur(40px); border-radius: 16px;
  box-shadow: inset 0 0 0 1px var(--glass-border); }

.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  background: var(--accent); color: #ffffff; border: none; border-radius: 12px; min-height: 44px;
  padding: 10px 16px; font-family: inherit; font-size: 15px; font-weight: 600; cursor: pointer;
  text-decoration: none; box-shadow: inset 0 0.5px 0 rgba(255,255,255,0.3); }
.btn:hover { color: #ffffff; } .btn:active { background: rgb(41,98,217); }
.btn:disabled { opacity: 0.6; cursor: default; }
.btn-block { display: flex; width: 100%; min-height: 48px; }
.btn-sm { min-height: 34px; padding: 0 16px; font-size: 13px; border-radius: 8px; }

.controls { display: flex; gap: 8px; align-items: center; justify-content: space-between;
  flex-wrap: wrap; margin-top: 4px; }
.controls .btn, .controls .search-input { height: 36px; min-height: 36px; }
.controls .btn { font-size: 13px; padding: 0 15px; }

.search-input { flex: 1 1 200px; min-width: 0; font-family: inherit; font-size: 13px;
  font-weight: 500; color: var(--text); height: 34px; padding: 0 13px; border: none;
  border-radius: 999px; background: var(--chip);
  box-shadow: inset 0 0 0 1px var(--glass-border), 0 0.5px 1px rgba(0,0,0,0.06); }
.search-input::placeholder { color: var(--text3); }
.search-input:focus { outline: none; box-shadow: inset 0 0 0 1px var(--glass-border), 0 0 0 3px rgba(0,122,255,0.4); }

.subtitle { margin: -6px 0 0; font-size: 13px; color: var(--text2); line-height: 1.5; }

/* Full-screen map landing (owner pivot, 2026-08-11): #map fills the whole
   viewport; every other element on / is a floating overlay above it, each
   one an independent position: fixed layer (not nested under a shared
   wrapper -- position: fixed resolves against the viewport from anywhere in
   the DOM here, since no ancestor sets a transform/filter/perspective that
   would change that).
   z-index: 0 on #map (not just position: relative, which Leaflet already
   sets on #map itself) is load-bearing: a positioned element only seals its
   descendants into their own stacking context once it has an explicit
   z-index, not "auto". Without this, Leaflet's internal panes/popups/zoom
   control (z-index up to ~1000) leak out of #map and paint over positioned
   siblings regardless of their own z-index or DOM order -- confirmed
   empirically while building the original (non-full-screen) version of
   this map, which is why every overlay below gets an explicit z-index. */
#map { position: fixed; inset: 0; z-index: 0; background: var(--map-bg); }
.leaflet-container { font-family: inherit; }

/* Leaflet popup content, restyled to match site typography instead of
   Leaflet's default chrome; dark-mode wrapper/tip colors live in the
   dark-mode block up top. */
.leaflet-popup-content-wrapper { border-radius: 12px; }
.leaflet-popup-content { margin: 10px 12px; font-family: inherit; font-size: 12.5px; line-height: 1.5; }

/* Record-ring popup action link (owner round 7, 2026-08-11): a real
   <button> styled as a link -- it opens the modal via JS, it doesn't
   navigate, so it isn't a real href (same convention as .overview-src-link
   for the Sources button). */
.popup-action { display: inline-block; margin-top: 4px; background: none; border: none; padding: 0;
  font: inherit; font-size: inherit; color: var(--link); cursor: pointer; text-decoration: underline; }
.popup-action:hover { color: var(--link-h); }

/* Landlord-on-record icon chip (item 3b, owner directive, 2026-08-12):
   replaces the plain dark-red ring dot -- L.divIcon, same person glyph as
   the Propriétaire card's own icon chip (public/app.js's CICON.proprio).
   Persistent layer, all zooms, ~300 points -- real DOM is fine at that
   scale. Bug/311 glyphs moved off divIcons entirely (addendum, 2026-08-12:
   "no hide and seek with the zoom" reaffirmed absolute) onto
   public/app.js's raw-canvas SpriteCanvasLayer -- the sprites are drawn
   straight to a <canvas> via drawImage, no CSS classes of their own. */
.leaflet-div-icon.landlord-marker { background: none; border: none; }
.landlord-marker-chip { display: flex; align-items: center; justify-content: center; width: 22px; height: 22px;
  border-radius: 6px; background: #8b1a1a; box-shadow: 0 0 0 1.5px #fff, 0 2px 5px rgba(0,0,0,0.35); }
.landlord-marker-chip svg { width: 12px; height: 12px; stroke: #fff; fill: none; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round; }

/* Inspection marker icon chip (owner clarity fix, 2026-08-12: "tapping a
   green inspection dot shows only 'May 29, 2025 · 2.2/6' -- cryptic, icons
   must carry meaning"). Same L.divIcon mechanism as the landlord chip above
   (public/app.js's inspDivIcons) -- CICON.inspections' own check glyph,
   background color set inline per score band (good/warn/bad), same pattern
   the map legend already uses for per-entry colors. Deliberately smaller
   and lighter-ringed than the landlord/bedbug chips -- inspections are
   context, not alarm, and should read as secondary at the same zoom. */
.leaflet-div-icon.insp-marker { background: none; border: none; }
.insp-marker-chip { display: flex; align-items: center; justify-content: center; width: 18px; height: 18px;
  border-radius: 5px; box-shadow: 0 0 0 1px #fff, 0 1px 3px rgba(0,0,0,0.3); }
.insp-marker-chip svg { width: 10px; height: 10px; stroke: #fff; fill: none; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round; }

/* Search icons (owner feedback round, 2026-08-11): the search input itself
   now lives inside the merged single top bar (.header-search, above) --
   these icon rules are shared with /proprietaire's own .search-field
   (not a pill over a map, so its icon sits absolutely inside a plain
   .search-input's padding instead of as a flex sibling). */
.search-icon { width: 14px; height: 14px; color: var(--text3); flex-shrink: 0; margin-left: 3px; }
.near-icon { width: 11px; height: 11px; flex-shrink: 0; }

.search-field { position: relative; flex: 1 1 200px; display: flex; align-items: center; min-width: 0; }
.search-field .search-icon { position: absolute; left: 13px; margin-left: 0; pointer-events: none; }
.search-field .search-input { flex: 1 1 auto; padding-left: 32px; }

/* Suggestions dropdown + hints: sit directly under the merged header bar
   now that there's no separate floating search pill between them and it
   -- --topbar-h (set by public/app.js from the header's real rendered
   height) already accounts for the search bar's own height since it's
   inside the header, so this only needs a small gap, not the old double
   offset. The 68px fallback only matters for the instant before that
   script runs. */
#sugg.glass-card { position: fixed; top: calc(var(--topbar-h, 68px) + 8px); left: 14px; right: 14px; z-index: 25; margin: 0; }
#pick-hint, #geo-msg { position: fixed; top: calc(var(--topbar-h, 68px) + 8px); left: 18px; right: 18px; z-index: 25;
  background: var(--glass-strong); border-radius: 10px; padding: 6px 10px; }

@media (min-width: 641px) {
  /* Matches the merged header bar's own capped-and-centered width (see
     body.page-home .site-header above) so the suggestions dropdown and
     hints line up under it exactly. */
  #sugg.glass-card, #pick-hint, #geo-msg { left: 50%; right: auto; width: 560px; max-width: calc(100vw - 28px); transform: translateX(-50%); }
}

/* Bottom-left stack (owner feature, 2026-08-12): the wrapper owns the
   fixed position + safe-area math that used to live on .map-legend alone
   -- the range picker and legend are separate glass-card children now,
   stacked via plain flexbox (no JS height math needed for either to know
   where the other ends). */
.map-bottom-controls { position: fixed; left: 14px; bottom: calc(env(safe-area-inset-bottom, 0px) + 14px); z-index: 20;
  max-width: calc(100vw - 28px); display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }
.map-bottom-controls > * { max-width: 100%; }
.map-legend { padding: 8px 12px; display: flex; flex-direction: column; gap: 5px; }
/* Date-range segmented pill (owner feature, 2026-08-12): same glass-card
   family as the legend, --accent fill on the active segment (matching
   every other "selected" affordance in the app -- theme toggle, sidebar
   active item). overflow-x:auto is a safety net at the narrowest phones
   (320px) where 5 segments plus padding could still be tight -- mirrors
   the sidebar's own mobile chip-row scroll pattern. */
.map-range { display: flex; gap: 2px; padding: 3px; overflow-x: auto; }
.range-seg { flex-shrink: 0; background: none; border: none; padding: 5px 10px; font: inherit; font-size: 11px;
  font-weight: 600; color: var(--text2); border-radius: 999px; cursor: pointer; white-space: nowrap; }
.range-seg.active { background: var(--accent); color: #ffffff; }
.range-seg:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.legend-dots { display: flex; flex-wrap: wrap; gap: 6px 12px; font-size: 10.5px; font-weight: 500; color: var(--text2); }
/* Category toggles (owner feature, 2026-08-12): every legend entry is a
   real <button class="legend-item"> now, not a plain <span> -- reset
   browser button chrome to keep the exact visual presentation the old
   spans had (same inline-flex/gap/nowrap this rule always set), then add
   the states a real interactive control needs. The OFF state keys
   directly off aria-pressed (public/app.js toggles it, no separate class
   needed) -- dimmed only, per the owner spec ("~40% opacity, just
   dimming, no strikethrough"). */
.legend-dots .legend-item { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
  background: none; border: none; padding: 0; margin: 0; font: inherit; color: inherit; text-align: left; cursor: pointer; }
.legend-dots .legend-item:hover { color: var(--text); }
.legend-dots .legend-item:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.legend-dots .legend-item[aria-pressed="false"] { opacity: .4; }
.legend-dots i { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
/* Mini bug chip (owner round 4, 2026-08-11), matching the map's own
   bug-icon markers -- same footprint as the plain 8px dots beside it. */
.legend-bug { width: 9px; height: 9px; border-radius: 2.5px; background: var(--bad);
  display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; }
.legend-bug svg { width: 6px; height: 6px; }
/* Inspection legend key (owner clarity fix, 2026-08-12: was 3 plain
   .legend-tridot <i> dots, now 3 .legend-bug check-glyph chips, same class
   the other icon-vocabulary entries already use) -- still clustered tight
   via the same negative-margin-on-2nd/3rd technique against the parent
   span's 5px flex gap, so they read as one "this varies" swatch, not three
   separate legend rows -- no single color is "correct" for a score that
   spans the full scale. */
.legend-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px;
  font-size: 10px; color: var(--text3); }
.legend-sources { background: none; border: none; padding: 0; font: inherit; font-size: 10px;
  color: var(--link); cursor: pointer; text-decoration: underline; }
/* Zoom-aware legend, inspection entry only (owner ruling, 2026-08-12: every
   bedbug legend entry lost its own .legend-detail-only class this round --
   the always-on canvas layer means they're never gated by zoom anymore.
   .legend-overview-only and its quartier entry are retired entirely, not
   just unused -- see the markup comment in src/render/home.js). Hidden by
   default (matches the page's own initial overview zoom, where inspections
   don't render yet); app.js flips .map-legend.detail-zoom once the map
   crosses isDetailZoom. CSS-only swap, no DOM rebuild. */
/* .legend-dots .legend-item (above) and .legend-dots .legend-detail-only
   (below) are equal specificity (two classes each) -- the hide rule wins
   by simply coming later in source order, same reasoning as before this
   round's .legend-item conversion, just no longer keyed off a bare
   element selector. */
.legend-dots .legend-detail-only { display: none; }
.map-legend.detail-zoom .legend-dots .legend-detail-only { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
/* Mobile legend collapse (dogfood fix, 2026-08-12): the toggle button only
   exists visually below 640px -- above that the legend just stays open and
   #legend-toggle is hidden, so its click handler in app.js is simply never
   reached. Below 640px .legend-dots starts collapsed; app.js toggles
   .legend-expanded on this same .map-legend element (via the button's
   closest()) to reveal it. */
.legend-toggle { display: none; }
@media (max-width: 640px) {
  .legend-toggle { display: block; background: none; border: none; padding: 0; margin: 0;
    font: inherit; font-size: 10.5px; font-weight: 600; color: var(--text2); cursor: pointer; }
  .legend-toggle::after { content: ' \25be'; }
  .legend-toggle.expanded::after { content: ' \25b4'; }
  .map-legend .legend-dots { display: none; }
  .map-legend.legend-expanded .legend-dots { display: flex; }
}
.legend-sources:hover { color: var(--link-h); }

/* Verdict cards overlay (Task 15): a bottom sheet on mobile (slides up over
   the lower portion of the map), a centered modal on desktop. Both shapes
   are the same #sheet element -- only its position/sizing changes at the
   breakpoint, so app.js's open/close logic never has to know which shape
   it's driving. No slide-in animation (a plain hidden-attribute toggle,
   same pattern as #sugg/#geo-msg above) -- deliberately scoped out to keep
   the interaction surface small; see the task report.

   Owner sidebar round (2026-08-11, macOS System Settings pattern): #sheet
   now holds a sidebar (#sheet-sidebar, a vertical list on desktop / a
   horizontal scrollable chip row on mobile -- same DOM/CSS class, just
   flex-direction changes at the breakpoint) plus a content pane
   (.sheet-pane). #sheet-title is the CURRENT SECTION's own title (Aperçu
   by default); #sheet-sub always holds the checked address + the
   corner-level honesty caption, regardless of section. */
#sheet-backdrop { position: fixed; inset: 0; z-index: 35; background: rgba(0,0,0,0.35); }
.sheet-close-btn { position: absolute; top: 8px; right: 10px; width: 30px; height: 30px; border-radius: 50%;
  border: none; background: var(--chip); color: var(--text2); font-size: 18px; line-height: 1; cursor: pointer;
  display: flex; align-items: center; justify-content: center; z-index: 2; }
.sheet-close-btn:hover { color: var(--text); }
/* Owner feedback (2026-08-12): max-height alone is a ceiling, not a floor
   -- the mobile sheet was shrinking to fit short sections (Loyer, Airbnb)
   and growing for tall ones (Punaises' chart, Aperçu's row list), same
   jumping-frame bug the desktop modal had (see the min-width:641px
   override below). Fixed height instead, in the middle of the 70-75%
   viewport range the brief asked for -- .sheet-body's own overflow-y:auto
   (unchanged) absorbs whichever section's content is taller than that. */
#sheet { position: fixed; left: 0; right: 0; bottom: 0; z-index: 40; height: 75vh;
  background: var(--glass-strong); backdrop-filter: saturate(180%) blur(30px);
  -webkit-backdrop-filter: saturate(180%) blur(30px); border-radius: 20px 20px 0 0;
  box-shadow: 0 -8px 30px rgba(0,0,0,0.25), inset 0 0 0 1px var(--glass-border);
  display: flex; flex-direction: column; }
.sheet-handle { width: 36px; height: 4px; border-radius: 999px; background: var(--text3);
  margin: 10px auto 0; flex-shrink: 0; cursor: pointer; }
.sheet-shell { display: flex; flex-direction: column; flex: 1; min-height: 0; }

/* Sidebar / mobile chip row: 8 buttons (Aperçu + the 7 verdict-card kinds),
   each icon + label + a real-data badge. Mobile-first here (row, scrolls
   horizontally, "pinned under the sheet handle" per the brief -- it's the
   first child of .sheet-shell, directly below .sheet-handle); the desktop
   media query below flips it to a vertical column. */
.sheet-sidebar { display: flex; flex-direction: row; gap: 6px; overflow-x: auto; overflow-y: hidden;
  -webkit-overflow-scrolling: touch; padding: 8px 14px 10px; flex-shrink: 0;
  box-shadow: inset 0 -0.5px 0 var(--hairline); }
@media (max-width: 640px) {
  /* Close-button overlap fix (dogfood fix, 2026-08-12, item 29): #sheet-
     close is absolutely positioned (top:8px, 30px tall) against #sheet
     itself, not against this row -- on mobile the chip row's own top
     padding (8px) put its content almost directly under the button,
     visually colliding with (and partly un-tappable behind) whichever
     chip landed at the row's right edge. Extra top padding here just
     pushes the whole row's content below the button's 38px-from-sheet-top
     footprint; nothing else about the row's own sizing changes. */
  .sheet-sidebar { padding-top: 30px; }
  /* Tab-scroll fade affordance (item 29): mask-image, not a real overlay
     element, so it never intercepts taps. .fade-start/.fade-end are
     toggled by app.js's updateSidebarFade off actual scrollLeft, not a
     static always-on fade -- no fade at the end you can't scroll past. */
  .sheet-sidebar.fade-start { -webkit-mask-image: linear-gradient(to right, transparent, black 24px); mask-image: linear-gradient(to right, transparent, black 24px); }
  .sheet-sidebar.fade-end { -webkit-mask-image: linear-gradient(to left, transparent, black 24px); mask-image: linear-gradient(to left, transparent, black 24px); }
  .sheet-sidebar.fade-start.fade-end { -webkit-mask-image: linear-gradient(to right, transparent, black 24px, black calc(100% - 24px), transparent); mask-image: linear-gradient(to right, transparent, black 24px, black calc(100% - 24px), transparent); }
}
.sidebar-item { display: flex; align-items: center; gap: 6px; border: none; background: none;
  border-radius: 999px; padding: 5px 10px 5px 5px; cursor: pointer; font: inherit; color: var(--text2);
  white-space: nowrap; flex-shrink: 0; }
.sidebar-item:hover { background: var(--row-hover); color: var(--text); }
.sidebar-item.active { background: var(--chip); color: var(--text); font-weight: 600; }
.sidebar-item .cicon { width: 20px; height: 20px; border-radius: 6px; flex-shrink: 0; }
.sidebar-item .cicon svg { width: 11px; height: 11px; }
.si-label { font-size: 12.5px; }
.si-badge { font-size: 10px; font-weight: 700; padding: 1px 7px; border-radius: 999px;
  background: var(--chip); color: var(--text3); flex-shrink: 0; }
.si-badge.good { color: var(--good); background: var(--good-bg); }
.si-badge.warn { color: var(--warn); background: var(--warn-bg); }
.si-badge.bad { color: var(--bad); background: var(--bad-bg); }
.si-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--bad);
  box-shadow: 0 0 0 2px var(--bad-bg); flex-shrink: 0; }

.sheet-pane { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.sheet-header { padding: 4px 44px 10px 20px; flex-shrink: 0; box-shadow: inset 0 -0.5px 0 var(--hairline); }
.sheet-title { margin: 0; font-size: 20px; font-weight: 800; letter-spacing: -0.02em; }
.sheet-header .sub { margin: 2px 0 0; font-size: 11.5px; color: var(--text2); line-height: 1.45; }

/* Tap-to-nearest-address provenance banner (owner round 6, 2026-08-11):
   "adresse la plus proche du point touché" plus up to 4 one-tap alternate
   chips ("Pas le bon immeuble ?"). Hidden/empty unless app.js's
   renderProvenance actually has something to say. */
.sheet-provenance { margin: 6px 0 0; }
.provenance-line { margin: 0; font-size: 11px; color: var(--link); font-weight: 600; }
.provenance-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 5px; }
.provenance-chips-label { font-size: 11px; color: var(--text3); flex-shrink: 0; }
.provenance-chip { font: inherit; font-size: 11.5px; color: var(--text); background: var(--chip);
  border: none; border-radius: 999px; padding: 4px 10px; cursor: pointer; white-space: nowrap; }
.provenance-chip:hover { background: var(--row-hover); }

/* Signalements 311 section (Task 17 pull-forward, owner directive
   2026-08-12): category chips reuse .provenance-chip's exact visual
   language (same var(--chip) pill) but are non-interactive (span, not
   button) -- these describe counts, they don't pick anything. */
.c311-categories, .c311-dates { margin-top: 8px; }
.c311-category-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 5px; }
.c311-category-chip { font-size: 11.5px; color: var(--text); background: var(--chip);
  border-radius: 999px; padding: 4px 10px; white-space: nowrap; }
.c311-dates-list { list-style: none; margin: 5px 0 0; padding: 0; display: flex; flex-direction: column; gap: 3px; }
.c311-dates-list li { font-size: 12.5px; color: var(--text2); line-height: 1.5; }

.sheet-body { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
  padding: 12px 14px 28px; }

/* Section content: every section but Aperçu adds .single to .verdict-stack
   (full-width, one card -- see the desktop masonry override below, which
   only applies without it). */
#section-content.verdict-stack { display: flex; flex-direction: column; gap: 10px; }

/* Overview (owner round 3, design mock v2, macOS Settings style): a
   synthesized summary, not the card list -- honesty strip (compact, one
   muted line), one verdict sentence (kept from round 2), a settings-style
   list of status rows (.overview-list/.overview-row, replacing round 2's
   stat-tile grid -- mirrors the mock's .grp/.grow), a source/freshness
   footer line. Deliberately its own class, not .verdict-stack, so the
   desktop masonry column-count rule below (which only makes sense for a
   stack of .verdict cards) never applies to it. */
#section-content.overview-content { display: flex; flex-direction: column; gap: 4px; }
.verdict-sentence { margin: 4px 0 2px; font-size: 15.5px; font-weight: 600; line-height: 1.42;
  letter-spacing: -0.005em; color: var(--text); }

.overview-list { background: var(--card-solid); border-radius: 10px; overflow: hidden;
  box-shadow: 0 0.5px 2.5px rgba(0,0,0,0.10), 0 0 0 0.5px var(--hairline); margin: 8px 0 6px; }
@media (prefers-color-scheme: dark) {
  .overview-list { box-shadow: 0 0.5px 2.5px rgba(0,0,0,0.35), 0 0 0 0.5px var(--hairline); }
}
html.dark .overview-list { box-shadow: 0 0.5px 2.5px rgba(0,0,0,0.35), 0 0 0 0.5px var(--hairline); }
html.light .overview-list { box-shadow: 0 0.5px 2.5px rgba(0,0,0,0.10), 0 0 0 0.5px var(--hairline); }
.overview-row { display: flex; align-items: center; gap: 10px; width: 100%; padding: 9px 13px;
  border: none; background: none; cursor: pointer; font: inherit; font-size: 13px; color: var(--text); text-align: left; }
.overview-row:hover { background: var(--row-hover); }
.overview-row + .overview-row { box-shadow: inset 0 0.5px 0 var(--hairline); }
.overview-row .tl { display: flex; align-items: center; gap: 9px; min-width: 0; flex: 1; }
.overview-row .cicon { width: 22px; height: 22px; border-radius: 5.5px; flex-shrink: 0; }
.overview-row .cicon svg { width: 13px; height: 13px; }
.row-label { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Overview-row subtitle (dogfood fix, 2026-08-12, item 31): only "Le Jeu"
   uses this today (buildOverviewRow's optional 5th arg) -- its name alone
   doesn't say what the section is, unlike the other rows' self-explanatory
   labels. min-width:0 lets the label's own ellipsis still work inside the
   wrap, same as it did as a direct .tl child. */
.row-label-wrap { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.row-label-wrap .row-label { line-height: 1.2; }
.row-sub { font-size: 11px; font-weight: 400; color: var(--text3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.overview-row .badge { flex-shrink: 0; }
.row-val { font-size: 11px; color: var(--text2); font-variant-numeric: tabular-nums; flex-shrink: 0; }
.row-chev { width: 7px; height: 11px; color: var(--text3); flex-shrink: 0; display: flex; }
.row-chev svg { display: block; width: 100%; height: 100%; }

.overview-src { margin: 8px 4px 0; font-size: 10.5px; color: var(--text3); }
.overview-src-link { background: none; border: none; padding: 0; font: inherit; font-size: inherit;
  color: var(--link); cursor: pointer; text-decoration: underline; }
.overview-src-link:hover { color: var(--link-h); }

/* Embedded Propriétaire search + Le Jeu plays (owner sidebar round): the
   sidebar's own sections re-use the standalone pages' components, styled
   to sit inside the modal/sheet the same way a verdict card does. */
/* Dogfood fix (owner review, 2026-08-12): .search-field's own
   `flex: 1 1 200px` was authored for its original horizontal context (the
   header search bar, a flex ROW) -- reused here inside .verdict, which is
   flex-direction: column, that same 200px flex-basis lands on the CROSS
   axis instead: a 200px-tall field with the actual ~34px input pinned to
   its top, and ~165px of dead space before the results below it. `flex:
   none` opts this instance out of the inherited sizing entirely. */
.embedded-search-field { flex: none; margin: 2px 0 8px; }
.embedded-search-field .search-input { height: 34px; }
.embedded-results:empty { display: none; }
#section-content .plays { margin-top: 2px; }

#sources-modal { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; }
#sources-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.4); }
.sources-content { position: relative; z-index: 1; width: min(440px, calc(100vw - 40px)); max-height: 80vh;
  overflow-y: auto; padding: 22px 22px 18px; }
.sources-content h2 { margin: 0 0 10px; font-size: 16px; }

@media (min-width: 641px) {
  /* Centered modal, not docked to a viewport edge -- the top-edge/
     backdrop-filter overlap workaround the old right-panel needed
     (--topbar-h offset on the close button) no longer applies since the
     modal never touches the floating header pill. */
  /* Owner feedback (2026-08-12): the modal used to size to its own content
     (max-height as a ceiling, never a floor) -- Loyer's two sentences drew
     a short box, Aperçu's 7-row list drew a tall one, and switching
     sections between them made the whole frame visibly jump. Fixed height
     now, not a cap: the frame is exactly this size regardless of which
     section is active, and .sheet-body's own overflow-y:auto (unchanged)
     is what absorbs a section's content being taller than that. */
  #sheet { left: 50%; right: auto; top: 50%; bottom: auto; transform: translate(-50%, -50%);
    width: min(820px, calc(100vw - 48px)); height: min(760px, 84vh);
    border-radius: 18px; box-shadow: 0 16px 48px rgba(0,0,0,0.30), inset 0 0 0 1px var(--glass-border); }
  .sheet-handle { display: none; }
  .sheet-shell { flex-direction: row; }
  .sheet-sidebar { flex-direction: column; width: 190px; flex-shrink: 0; overflow-x: hidden; overflow-y: auto;
    padding: 16px 10px; gap: 2px; box-shadow: inset -0.5px 0 0 var(--hairline); }
  .sidebar-item { border-radius: 8px; padding: 6px 8px; white-space: normal; }
  .si-label { flex: 1; min-width: 0; }
  .sheet-header { padding: 18px 44px 14px 22px; }
  /* Two-column masonry for the Aperçu stack (mock: .stack.grid) -- CSS
     multi-column, not flex/grid, so cards of uneven height still pack
     tightly instead of leaving one column's row heights dictate the
     other's. Never applies to a .single section (one card, full pane
     width) or to /proprietaire's own .verdict-stack result list (a
     normal scrolling page, not this modal, and never carries the
     #section-content id). */
  #section-content.verdict-stack:not(.single) { display: block; column-count: 2; column-gap: 10px; }
  #section-content.verdict-stack:not(.single) > .verdict { break-inside: avoid; margin-bottom: 10px; }
}
@media (min-width: 641px) and (max-width: 900px) {
  /* Below ~900px there isn't enough width left for two genuinely readable
     columns once the sidebar + the modal's own padding is accounted for --
     single column until the modal has real room to spare. */
  #section-content.verdict-stack:not(.single) { column-count: 1; }
}

/* Dogfood fix (owner review, 2026-08-12): was bare colored text with no
   card behind it -- now shares .glass-card (blur/border/shadow) with the
   search suggestions dropdown, same floating-card language as the rest of
   this bar's overlays. */
.geo-error { font-size: 12px; color: #d03b3b; line-height: 1.5; }
@media (prefers-color-scheme: dark) { .geo-error { color: var(--bad); } }
.pick-hint { margin: -4px 0 0; font-size: 12px; color: var(--text2); line-height: 1.5; }

/* search suggestions dropdown */
.sugg-list { position: relative; z-index: 10; padding: 4px 0; margin-top: -4px; overflow: hidden; }
.sugg-item { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; width: 100%;
  text-align: left; border: none; cursor: pointer; font-family: inherit; font-size: 13.5px;
  color: var(--text); background: transparent; padding: 10px 14px; }
.sugg-item:hover, .sugg-item.active { background: var(--row-hover); }
.sugg-item + .sugg-item { box-shadow: inset 0 0.5px 0 var(--hairline); }
.sugg-hint { color: var(--text3); font-size: 11.5px; flex-shrink: 0; }
/* Suffixed-civic note (dogfood fix, 2026-08-12, item 32): "affiché : 3720"
   sits above the results list itself, not inside .sugg-empty -- unlike the
   nearest-chip/coverage notes above, this fires when there ARE results, on
   a real hit that just resolved a typed unit-suffix letter away. */
.sugg-suffix-note { margin: 0; padding: 8px 14px 0; font-size: 11.5px; color: var(--text3); }
.sugg-empty { padding: 10px 14px; }
.sugg-empty-line { margin: 0; font-size: 13px; color: var(--text3); }
/* Coverage note (dogfood fix, 2026-08-12): a second, quieter line under
   "no address found" naming the real limit (Montreal-only) instead of
   leaving a Gatineau/off-island search reading like a typo. */
.sugg-coverage { margin: 3px 0 0; font-size: 11.5px; color: var(--text3); }
/* Civic-miss "nearest" chips (dogfood fix, 2026-08-12, wave A's apiSearch
   nearest field): "3465 introuvable -- le plus proche : 3475" instead of a
   dead end when the street matches but the exact civic number doesn't.
   Same visual language as .provenance-chip below (one-tap alternate
   addresses), just scoped to the search dropdown's own empty state. */
.sugg-nearest-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 7px; }
.sugg-nearest-chip { font: inherit; font-size: 11.5px; color: var(--text); background: var(--chip);
  border: none; border-radius: 999px; padding: 4px 10px; cursor: pointer; white-space: nowrap; }
.sugg-nearest-chip:hover { background: var(--row-hover); }

/* verdict cards + freshness/source rows (Task 15: solid cards per the mock,
   sitting inside the glass sheet/modal for contrast -- see the header
   comment above .card-top for why this overrides .glass-card via cascade
   order instead of a DOM class change). verdict--{accent} no longer draws a
   left border stripe: status now reads through the badge chip and the
   card's own icon-chip color, matching the mock; the modifier class stays
   on every card as a semantic/JS hook. */
.verdict-stack { display: flex; flex-direction: column; gap: 10px; }
.verdict { background: var(--card-solid); backdrop-filter: none; -webkit-backdrop-filter: none;
  border-radius: 12px; box-shadow: 0 0.5px 2.5px rgba(0,0,0,0.10), 0 0 0 0.5px var(--hairline);
  padding: 13px 15px; display: flex; flex-direction: column; gap: 8px; }
@media (prefers-color-scheme: dark) {
  .verdict { box-shadow: 0 0.5px 2.5px rgba(0,0,0,0.35), 0 0 0 0.5px var(--hairline); }
}
html.dark .verdict { box-shadow: 0 0.5px 2.5px rgba(0,0,0,0.35), 0 0 0 0.5px var(--hairline); }
html.light .verdict { box-shadow: 0 0.5px 2.5px rgba(0,0,0,0.10), 0 0 0 0.5px var(--hairline); }

/* icon-chip card header: tinted square + title on the left, status badge
   pill on the right */
.card-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.card-top .tl { display: flex; align-items: center; gap: 8px; min-width: 0; }
.cicon { width: 24px; height: 24px; border-radius: 6px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
.cicon svg { width: 14px; height: 14px; stroke: #fff; fill: none; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.badge { font-size: 10px; font-weight: 700; padding: 2px 8px; border-radius: 999px; white-space: nowrap; flex-shrink: 0; }
.badge.good { color: var(--good); background: var(--good-bg); }
.badge.warn { color: var(--warn); background: var(--warn-bg); }
.badge.bad { color: var(--bad); background: var(--bad-bg); }
.badge.na { color: var(--text3); background: var(--chip); }

/* 6-segment inspection gauge */
.gauge { display: flex; gap: 3px; margin: 2px 0; }
.gauge i { flex: 1; height: 6px; border-radius: 999px; background: var(--bar-track); display: block; }
.gauge i.on.good { background: var(--good); }
.gauge i.on.warn { background: var(--warn); }
.gauge i.on.bad { background: var(--bad); }

.card-src { display: flex; justify-content: flex-end; align-items: baseline; gap: 6px;
  flex-wrap: wrap; font-size: 11.5px; color: var(--text3); margin-top: 8px; }
.card-src a { color: inherit; }
.card-src a:hover { color: var(--text); }

/* Compact/muted (owner round 2, 2026-08-11): was a full-size paragraph
   leading the old Aperçu card stack; now it's one small line under the
   Overview header, since the verdict sentence + stat tiles carry the
   at-a-glance weight instead. */
.honesty-strip { font-size: 11px; color: var(--text3); line-height: 1.4; margin: 2px 0 0; }
.freshness-line { font-size: 11.5px; color: var(--text3); margin: -4px 0 0; }
/* Status-chip legend footnote (dogfood fix, 2026-08-12): what "à
   surveiller"/"au dossier" actually mean -- same muted register as the
   honesty strip. */
.chip-legend-note { font-size: 10.5px; color: var(--text3); line-height: 1.4; margin: 2px 4px 0; }

footer { max-width: 680px; margin: 0 auto; padding: 18px 14px 36px; color: var(--text2);
  font-size: 12px; line-height: 1.6; box-shadow: inset 0 0.5px 0 var(--hairline); }
footer p { margin: 0 0 4px; }
.foot-title { font-weight: 600; color: var(--text); }
.src-links a { color: inherit; margin-right: 2px; }
.src-links a:hover { color: var(--text); }

/* comparison rows (per-card mini bars, e.g. bedbugs 5-yr vs city curve) */
.vrow { padding: 5px 0; }
.vrow + .vrow { box-shadow: inset 0 0.5px 0 var(--hairline); }
.vrow-top { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; font-size: 12.5px; }
.vrow-name { min-width: 0; line-height: 1.35; }
.vrow.me .vrow-name { font-weight: 700; }
.vrow-val { flex-shrink: 0; color: var(--text2); font-variant-numeric: tabular-nums; font-size: 11.5px; }
.vrow-bar { height: 4px; border-radius: 999px; background: var(--bar-track); position: relative; overflow: hidden; margin-top: 5px; }
.vrow-bar i { position: absolute; inset: 0 auto 0 0; border-radius: 999px; }

/* verdict card content rows (Task 9: titles, plain lines, caveats, facts) */
.card-title { margin: 0; font-size: 15px; font-weight: 700; letter-spacing: -0.01em; }
.card-line { margin: 0; font-size: 13.5px; line-height: 1.5; }
.card-note { margin: 0; font-size: 12px; color: var(--text2); line-height: 1.5; }
.card-play { margin: 0; font-size: 13px; font-weight: 500; line-height: 1.5; }
.card-flag { margin: 0; font-size: 12px; color: var(--bad); line-height: 1.4; }
.card-row { padding: 6px 0; display: flex; flex-direction: column; gap: 3px; font-size: 13px; }
.card-row + .card-row { box-shadow: inset 0 0.5px 0 var(--hairline); }
.card-row a { font-size: 12px; }
/* bâtiment's 2-column facts grid (Task 15) -- also reused for the
   inspection card's six per-component scores (.insp-scores, anti-opacity
   ruling, 2026-08-11). */
.facts { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 14px; }
.fact .fl { font-size: 10.5px; color: var(--text2); }
.fact .fv { font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; }
.insp-scores { margin-top: 6px; }
/* Divider between uef units at one civic address (dogfood fix,
   2026-08-12) -- same hairline convention .card-row + .card-row already
   uses, not a bare <hr> (which barely reads as a divider here). */
.facts + .facts { margin-top: 10px; padding-top: 10px; box-shadow: inset 0 0.5px 0 var(--hairline); }

/* Conviction "see full text" expand (anti-opacity ruling, 2026-08-11): a
   plain <details>/<summary> -- native keyboard/screen-reader support, no
   JS state to manage. summary itself is styled as a small link; the
   revealed text reuses .card-note typography. */
.record-expand { margin-top: 2px; }
.record-expand summary { font-size: 11.5px; color: var(--link); cursor: pointer;
  list-style: none; }
.record-expand summary::-webkit-details-marker { display: none; }
.record-expand summary:hover { color: var(--link-h); }
.record-expand[open] summary { margin-bottom: 4px; }
.record-expand .card-note { white-space: pre-line; }

/* Landlord section's three states (owner round 5, 2026-08-11): .card-lead
   is a small bold lead-in line ("Nom publié pour cette adresse :", the
   embedded search's "Vous avez un nom ?"); .card-list is the "where to
   find it" explainer bullets in the no-name state. */
.card-lead { margin: 0; font-size: 12.5px; font-weight: 700; color: var(--text2); }
.card-list { margin: 2px 0 0; padding-left: 18px; font-size: 12.5px; color: var(--text2); line-height: 1.55; }
.card-list li + li { margin-top: 3px; }
.proprio-names { display: flex; flex-direction: column; gap: 10px; }
.proprio-name-block + .proprio-name-block { box-shadow: inset 0 0.5px 0 var(--hairline); padding-top: 8px; }
.proprio-name { margin: 0 0 2px; font-size: 15px; font-weight: 700; letter-spacing: -0.01em; }

/* /guide "Le Jeu" (Task 11): six plays as glass-card list items. Numbered
   badge (Task 15 stage 5, per the mock's .play h3::before) via a CSS
   counter scoped to .play-title -- decimal-leading-zero to read "01"/"02"
   like the mock, not a plain "1"/"2". */
.plays { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; counter-reset: play; }
.play { padding: 16px 18px; display: flex; flex-direction: column; gap: 6px; }
.play-title { display: flex; gap: 9px; align-items: baseline; }
.play-title::before { counter-increment: play; content: counter(play, decimal-leading-zero);
  font-size: 11px; font-weight: 800; color: var(--text3); font-variant-numeric: tabular-nums; flex-shrink: 0; }
.play-link { margin: 4px 0 0; font-size: 12px; }
.play-link a { font-weight: 600; }

@media (max-width: 420px) {
  h1 { font-size: 21px; }
}
