:root {
  /* Layout dimensions (ALIGNMENT.md §1). Deliberately on :root, not on the section wrapper:
     --content-inset used to live on .card, so removing the card would have silently broken
     all 17 places that use it. */
  /* User requirement: filters/results should ALWAYS align with the header bar (Home/Theme
     button) — "toolbar as the reference for the max width." A separate --browse-max just for
     the Browse page (first attempt) broke that, because .site-toolbar-inner kept using
     --strip-max. Now there's ONE shared value for the header bar, home hero, Compare page,
     AND Browse layout — that way they always stay mathematically aligned, no two widths that
     can drift apart. Was briefly 1600px (an attempt to use screen width better), but on
     wide/external displays that left almost no breathing room left/right at the screen edge
     ("no breathing room at the edges") — back to the original 1200px value, which leaves a
     noticeable margin on large screens again. */
  --strip-max: 1200px;
  --page-gutter: 24px;
  /* Width of the row marker plus its gap: step badge 28px + 14px, or icon-chip 34px + 8px =
     42px. That's where the TITLE TEXT starts. */
  --marker-inset: 42px;
  /* On wide screens, content starts at the same spot as the title text, i.e. at the marker
     inset. On narrow screens this becomes 0 (see the media query at the end of the file) —
     hence the two separate variables. */
  --content-inset: var(--marker-inset);
  /* Seam line between two sections (ALIGNMENT.md §5): opacity-based white, no fixed hex value
     per theme — a single definition for both modes.
     Measured at the Filter→Results seam (contrast of the line against the background above/below):
       light: 0.30 ≈ 1.2   (even at 1.0 it's only 1.38 — on the near-white gradients, white
                            inevitably stays invisible, more opacity doesn't help there)
       dark:  0.30 ≈ 3.8   (clearly visible; 0.60 would already be harsh at ~6.9)
     Hence the lower end of the given range: in dark mode the line should stay subdued. See
     ALIGNMENT.md §5 for the asymmetry between the modes. */
  --section-divider: rgba(255, 255, 255, 0.30);

  /* Layer 9: color system spec (Light). --bg now carries EVERY section (Filter/Results/
     Sources/Compare) — the earlier per-section gradients are deliberately reduced to the same
     flat value (see --section-*-grad below); the brand color now only lives on interactive
     elements (buttons/active chips/links/badges) and in the hero gradient. */
  --bg: #f7f8f9;
  --card-bg: #ffffff;
  --text: #0f172a;
  --text-muted: #47535f;
  --border: #a7b4bd;
  /* "Secondary" from the spec: active states, selected chips, links, icons. */
  --accent: #14b8a6;
  --accent-light: #2dd4bf;
  --accent-dark: #0d9488;
  --accent-bg: #e6fbf6;
  --accent-contrast: #ffffff;
  --row-hover: #f0faf9;
  --header-bg: #edf6f5;
  --shadow: 0 1px 3px rgba(15, 23, 42, 0.08), 0 14px 32px -12px rgba(15, 23, 42, 0.22);
  --shadow-hover: 0 1px 3px rgba(15, 23, 42, 0.10), 0 24px 48px -16px rgba(15, 23, 42, 0.32);
  /* No more gradients outside the hero — every section carries the same flat page background.
     Variable names deliberately kept as-is (not renamed/removed) so every existing usage site
     stays unchanged and automatically picks up the new, flat value. */
  --section-filter-grad: var(--bg);
  --section-results-grad: var(--bg);
  --section-sources-grad: var(--bg);
  --section-hero-grad: linear-gradient(135deg, #0c4a6e 0%, #0369a1 30%, #0891b2 60%, #14b8a6 85%, #5eead4 100%);
  --panel-bg: rgba(255, 255, 255, 0.82);
  /* A dedicated, noticeably more transparent value just for the header bar (Layer 6, Part A:
     "transparent + blur" like on apple.com) — --panel-bg itself stays at 0.82 for modals/the
     funnel button, where opacity for readability over a changing background matters more than
     the glass effect. */
  --toolbar-bg: rgba(255, 255, 255, 0.55);
  --tab-inactive-bg: #cfd8dd;
  --tab-lift-shadow: 0 -2px 4px -3px rgba(15, 23, 42, 0.15);
  /* "Primary" from the spec: primary CTAs (Explore/Compare buttons), headings. */
  --primary: #0369a1;
  --primary-hover: #075985;
  --primary-contrast: #ffffff;
  /* A noticeably lighter tone ONLY for gradients (user requirement: "the Compare button
     gradient should look more visible/3D") — --primary/--primary-hover are too close together
     to stand out as a gradient. */
  --primary-light: #38bdf8;
  /* "Accent — Coral": secondary/utility actions (sort, reset, filter trigger).
     IMPORTANT: coral ALWAYS gets dark text (--coral-text), never white — identical in both
     themes, see the spec's contrast rule. */
  --coral: #f2735c;
  --coral-hover: #e35f47;
  --coral-text: #0f172a;
  /* "Accent — Gold": Top-X% badge, "featured" preset marker, filter-count badge — replaces the
     previous bespoke amber (--achievement-*) with the exact same spec value. Dark text like
     coral: gold is light/saturated enough that white would have too little contrast. */
  --gold: #fea816;
  --gold-text: #0f172a;
  /* Type color coding per the spec, now as a "soft badge" (user requirement): a light tint of
     the role color as the background + the same hue, saturated, as the text, instead of a
     solid fill + near-black text — fits better with the mostly-white background (Layer 9, §3)
     and reads less "muddy" than the old combination. */
  --type-gymnasium-bg: #eef1fd;
  --type-gymnasium-text: #4c5fd5;
  --type-stadtteilschule-bg: #e6fbf9;
  --type-stadtteilschule-text: #0d9488;
  /* Muted teal ONLY for the slider fill (user requirement: "muted, not saturated") — the
     handle/thumb deliberately stays var(--accent), since per spec handles keep the action
     color. */
  --accent-muted: rgba(20, 184, 166, 0.35);
  /* The filter-count badge stays gold (spec role "Accent-Gold: ... filter count badge"),
     unchanged. The Top-X% card badge (.card-achievement-badge) deviates from that and now has
     its OWN coral tokens (user requirement, an experiment) — so it's no longer an alias for
     --gold, but two separate roles. */
  --achievement-bg: var(--gold);
  --achievement-text: var(--gold-text);
  --achievement-badge-bg: #fdece7;
  --achievement-badge-text: #e35f47;
  /* Genuinely neutral gray for the three card metric boxes (user requirement: "not teal") —
     deliberately its own token instead of reusing --header-bg, which has a visible blue-green
     cast in light mode (#edf6f5) — exactly what should be avoided here. */
  --stat-block-bg: #f0f1f3;
}
@media (prefers-color-scheme: dark) {
  :root {
    /* Layer 9 (Dark): deep navy instead of pure black (spec), no more per-section gradients,
       hero gradient deliberately IDENTICAL to the light theme (see below). */
    --bg: #0b1622;
    /* --card-bg/--border/--shadow (user requirement, see the "1+2+3" artifact preview): in the
       dark theme, --bg/--card-bg/--border were practically the same brightness AND the drop
       shadow is invisible on a black background — the filter box/cards barely stood out from
       the background. card-bg lightened away from the page (lever 2), border raised noticeably
       (lever 1), --shadow additionally gets a faint light top-edge sheen (lever 3) — the
       standard trick for depth in dark mode, where a dark drop shadow alone shows nothing. */
    --card-bg: #17273a;
    --text: #f1f5f9;
    --text-muted: #8ea3a6;
    --border: #33484a;
    --accent: #2dd4bf;
    --accent-light: #5eead4;
    --accent-dark: #14b8a6;
    --accent-bg: rgba(45, 212, 191, 0.12);
    --accent-contrast: #04211d;
    --row-hover: rgba(45, 212, 191, 0.07);
    --header-bg: #1a2534;
    --shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 14px 32px -14px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.05);
    --shadow-hover: 0 1px 2px rgba(0, 0, 0, 0.45), 0 24px 48px -16px rgba(0, 0, 0, 0.75), inset 0 1px 0 rgba(255, 255, 255, 0.05);
    --section-filter-grad: var(--bg);
    --section-results-grad: var(--bg);
    --section-sources-grad: var(--bg);
    /* Deliberately the same gradient definition as the light theme (spec: "pixel-identical
       between themes, don't create a separate dark hero gradient"). */
    --section-hero-grad: linear-gradient(135deg, #0c4a6e 0%, #0369a1 30%, #0891b2 60%, #14b8a6 85%, #5eead4 100%);
    --panel-bg: rgba(17, 30, 46, 0.82);
    --toolbar-bg: rgba(17, 30, 46, 0.55);
    --tab-inactive-bg: #33454b;
    --tab-lift-shadow: 0 -2px 4px -3px rgba(0, 0, 0, 0.45);
    --primary: #38bdf8;
    --primary-hover: #0ea5e9;
    --primary-contrast: #04222f;
    --primary-light: #7dd3fc;
    --coral: #fb8a6c;
    --coral-hover: #f2735c;
    --coral-text: #0f172a;
    --gold: #fbbf24;
    --gold-text: #0f172a;
    --type-gymnasium-bg: rgba(139, 158, 232, 0.16);
    --type-gymnasium-text: #8b9ee8;
    --type-stadtteilschule-bg: rgba(34, 211, 196, 0.16);
    --type-stadtteilschule-text: #22d3c4;
    --accent-muted: rgba(45, 212, 191, 0.32);
    --achievement-bg: var(--gold);
    --achievement-text: var(--gold-text);
    --achievement-badge-bg: rgba(251, 138, 108, 0.16);
    --achievement-badge-text: #fb8a6c;
    --stat-block-bg: rgba(255, 255, 255, 0.06);
  }
}
:root[data-theme="dark"] {
  --bg: #0b1622;
  --card-bg: #17273a;
  --text: #f1f5f9;
  --text-muted: #8ea3a6;
  --border: #33484a;
  --accent: #2dd4bf;
  --accent-light: #5eead4;
  --accent-dark: #14b8a6;
  --accent-bg: rgba(45, 212, 191, 0.12);
  --accent-contrast: #04211d;
  --row-hover: rgba(45, 212, 191, 0.07);
  --header-bg: #1a2534;
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 14px 32px -14px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.05);
  --shadow-hover: 0 1px 2px rgba(0, 0, 0, 0.45), 0 24px 48px -16px rgba(0, 0, 0, 0.75), inset 0 1px 0 rgba(255, 255, 255, 0.05);
  --section-filter-grad: var(--bg);
  --section-results-grad: var(--bg);
  --section-sources-grad: var(--bg);
  --section-hero-grad: linear-gradient(135deg, #0c4a6e 0%, #0369a1 30%, #0891b2 60%, #14b8a6 85%, #5eead4 100%);
  --panel-bg: rgba(17, 30, 46, 0.82);
  --toolbar-bg: rgba(17, 30, 46, 0.55);
  --tab-inactive-bg: #33454b;
  --tab-lift-shadow: 0 -2px 4px -3px rgba(0, 0, 0, 0.45);
  --primary: #38bdf8;
  --primary-hover: #0ea5e9;
  --primary-contrast: #04222f;
  --primary-light: #7dd3fc;
  --coral: #fb8a6c;
  --coral-hover: #f2735c;
  --coral-text: #0f172a;
  --gold: #fbbf24;
  --gold-text: #0f172a;
  --type-gymnasium-bg: rgba(139, 158, 232, 0.16);
  --type-gymnasium-text: #8b9ee8;
  --type-stadtteilschule-bg: rgba(34, 211, 196, 0.16);
  --type-stadtteilschule-text: #22d3c4;
  --accent-muted: rgba(45, 212, 191, 0.32);
  --achievement-bg: var(--gold);
  --achievement-text: var(--gold-text);
  --achievement-badge-bg: rgba(251, 138, 108, 0.16);
  --achievement-badge-text: #fb8a6c;
  --stat-block-bg: rgba(255, 255, 255, 0.06);
}
:root[data-theme="light"] {
  --bg: #f7f8f9;
  --card-bg: #ffffff;
  --text: #0f172a;
  --text-muted: #47535f;
  --border: #a7b4bd;
  --accent: #14b8a6;
  --accent-light: #2dd4bf;
  --accent-dark: #0d9488;
  --accent-bg: #e6fbf6;
  --accent-contrast: #ffffff;
  --row-hover: #f0faf9;
  --header-bg: #edf6f5;
  --shadow: 0 1px 3px rgba(15, 23, 42, 0.08), 0 14px 32px -12px rgba(15, 23, 42, 0.22);
  --shadow-hover: 0 1px 3px rgba(15, 23, 42, 0.10), 0 24px 48px -16px rgba(15, 23, 42, 0.32);
  --section-filter-grad: var(--bg);
  --section-results-grad: var(--bg);
  --section-sources-grad: var(--bg);
  --section-hero-grad: linear-gradient(135deg, #0c4a6e 0%, #0369a1 30%, #0891b2 60%, #14b8a6 85%, #5eead4 100%);
  --panel-bg: rgba(255, 255, 255, 0.82);
  --toolbar-bg: rgba(255, 255, 255, 0.55);
  --tab-inactive-bg: #cfd8dd;
  --tab-lift-shadow: 0 -2px 4px -3px rgba(15, 23, 42, 0.15);
  --primary: #0369a1;
  --primary-hover: #075985;
  --primary-contrast: #ffffff;
  --primary-light: #38bdf8;
  --coral: #f2735c;
  --coral-hover: #e35f47;
  --coral-text: #0f172a;
  --gold: #fea816;
  --gold-text: #0f172a;
  --type-gymnasium-bg: #eef1fd;
  --type-gymnasium-text: #4c5fd5;
  --type-stadtteilschule-bg: #e6fbf9;
  --type-stadtteilschule-text: #0d9488;
  --accent-muted: rgba(20, 184, 166, 0.35);
  --achievement-bg: var(--gold);
  --achievement-text: var(--gold-text);
  --achievement-badge-bg: #fdece7;
  --achievement-badge-text: #e35f47;
  --stat-block-bg: #f0f1f3;
}
* { box-sizing: border-box; }
/* NO overflow-x on html (neither hidden nor clip) — clip also turned out to be a sticky killer:
   position:sticky broke not only on real Safari/iPad, but reproducibly in Chromium too, as soon
   as html carried any overflow-x other than visible (verified with Playwright: .sidebar-panel
   followed the scroll 1:1 instead of sticking, as soon as this line was set).
   The original reason for the line (a ~44px-wide horizontal overlap from .trend-nodata-tooltip,
   which used to run past the viewport edge as a nowrap row) is fixed at the source (see
   .trend-nodata-tooltip: max-width + line wrapping) — which also makes the originally-feared
   mobile bottom-sheet problem (position:fixed; inset:0 picking up the inflated width instead of
   the real device width) moot, with no need for this line at all. */
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  color: var(--text);
  /* No more padding: the sections run to the screen edge and bring their own vertical spacing
     (ALIGNMENT.md §1/§5). */
  padding: 0;
  /* Layer 9 (spec §3): "Remove all other gradients... apply consistently" — the two radial
     glow gradients used to sit BEHIND EVERY page (Browse/Compare/Sources too), not just behind
     the hero. Replaced with the flat page background. */
  background: var(--bg);
}
/* Just a flow container now, no more width limit — that now lives per-section on
   .section-inner, so the background stays edge-to-edge and only the text is constrained. */
.wrap { width: 100%; }

/* --- Full-bleed sections (ALIGNMENT.md §1) --------------------------------------------------
   .section carries the background + vertical spacing and runs from screen edge to screen edge;
   .section-inner is ONLY a reading width (centered, no background of its own, no border, no
   shadow — otherwise it would be a card again). */
.section {
  position: relative;
  padding: 40px 0;
}
/* Applies to EVERY seam (hero→filter, filter→results, results→sources), since the hero
   section is the first one and so is the only one with no predecessor. */
.section + .section { border-top: 1px solid var(--section-divider); }
.section-inner {
  max-width: var(--strip-max);
  margin: 0 auto;
  padding: 0 var(--page-gutter);
}
/* Layer 6: shared header bar, across pages (see the index.html comment on #siteToolbar).
   position:sticky instead of fixed+JS positioning: in its normal state it automatically
   reserves its own place in the flow (no jump in the content below it when it first sticks)
   and sticks itself to top:0 on its own as soon as it's about to scroll out of the viewport —
   JS only handles the show/hide via translateY per scroll direction
   (updateToolbarOnScroll() in app.js), not the positioning itself. */
.site-toolbar {
  position: sticky;
  top: 0;
  z-index: 250;
  background: var(--toolbar-bg);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--section-divider);
  transition: transform 0.28s ease;
}
.site-toolbar.is-hidden { transform: translateY(-100%); }
.site-toolbar-inner {
  max-width: var(--strip-max);
  margin: 0 auto;
  padding: 8px var(--page-gutter);
}
/* Fixed row: Home on the left, Language/Theme on the right. Deliberately NOT the same flex row
   as the dropdown menus (.toolbar-menu, see below) — only this way does its height stay
   constant regardless of whether a menu is open, and Home/triggers don't slide down when one
   opens. */
.toolbar-bar { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.toolbar-home {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  color: var(--text);
  font-size: 0.86rem;
  font-weight: 600;
  text-decoration: none;
  flex-shrink: 0;
  transition: color 0.15s ease, transform 0.15s ease;
}
.toolbar-home:hover { color: var(--accent-dark); transform: translateY(-1px); }
.toolbar-home:active { transform: scale(0.96); }
.toolbar-home svg { width: 17px; height: 17px; flex-shrink: 0; }
.toolbar-right { display: flex; align-items: center; gap: 8px; }
/* Schools/Compare: plain text links with no button chrome (no border/background) —
   deliberately the same look as .toolbar-home (color/weight/size), just without an icon. They
   live INSIDE .toolbar-right (before the existing lang/theme triggers), so .toolbar-bar's
   justify-content:space-between still only anchors Home on the left and this whole group on
   the right — the lang/theme triggers therefore stay completely unchanged in style/position/
   behavior. Weight dialed to 600 (instead of 700), slightly dimmed; the color was set to
   --text-muted as a test, then reverted to --text (normal text color) per explicit request —
   should NOT look gray. */
.toolbar-nav-link {
  color: var(--text);
  font-size: 0.86rem;
  font-weight: 600;
  text-decoration: none;
  transition: color 0.15s ease;
}
.toolbar-nav-link:hover { color: var(--accent-dark); }
/* Dedicated spacing ONLY between Schools and Compare, AND between Compare and the lang trigger
   (not via the shared .toolbar-right gap, which would otherwise also apply between Lang and
   Theme) — more generous than the usual 8px, so neither Schools+Compare nor Compare+Lang read
   as a matched pair (user requirement). */
.toolbar-nav-link-desktop:first-of-type { margin-right: 22px; }
.toolbar-nav-link-desktop:last-of-type { margin-right: 22px; }

/* Hamburger trigger for Schools/Compare (+ future further entries) on mobile: hidden by
   default, only visible < 600px (see the media query below). Deliberately FRAMELESS (no
   border/background/shadow like the lang/theme trigger) and larger than the original 17px
   icon — a plain icon glyph instead of a button chip, sits in the markup as the LAST child of
   .toolbar-right so it always sits furthest right on mobile (after Lang/Theme). */
.nav-menu-trigger {
  display: none;
  width: 40px;
  height: 40px;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  color: var(--text);
  cursor: pointer;
  transition: color 0.15s ease, transform 0.15s ease;
}
.nav-menu-trigger:hover { color: var(--accent-dark); }
.nav-menu-trigger:active { transform: scale(0.92); }
.nav-menu-trigger svg { width: 24px; height: 24px; }
/* #navMenu stacks vertically instead of wrapping row by row like the lang/theme menu — every
   new toolbar entry simply stacks below the others going forward (user requirement), no second
   column. */
.toolbar-menu-vertical { flex-direction: column; align-items: stretch; }
/* Deliberately FRAMELESS (no border/background like lang-btn/theme-option) — plain text rows,
   only a subtle fill on hover/tap as feedback. */
.toolbar-menu-link {
  display: flex;
  align-items: center;
  padding: 10px 4px;
  border-radius: 6px;
  border: none;
  background: transparent;
  color: var(--text);
  font-size: 0.88rem;
  font-weight: 600;
  text-decoration: none;
  transition: background-color 0.15s ease, color 0.15s ease;
}
.toolbar-menu-link:hover { background: var(--row-hover); color: var(--text); }
.toolbar-menu-link:active { background: var(--row-hover); }
@media (max-width: 600px) {
  .toolbar-nav-link-desktop { display: none; }
  .nav-menu-trigger { display: flex; }
  /* Home/Schools/Compare are a bit larger on mobile than the desktop base value (user
     requirement) — the Home icon grows at the same ratio (17px -> 20px, ~+18%, like the text
     0.86rem -> 1rem), otherwise it would look too small/out of proportion next to the larger
     text. */
  .toolbar-home { font-size: 1rem; }
  .toolbar-home svg { width: 20px; height: 20px; }
  .toolbar-menu-link { font-size: 1rem; }
}
/* Tablet/iPad only — Home/Schools/Compare noticeably larger here than the plain desktop value
   (0.86rem). Two earlier attempts with fixed width breakpoints (0.94rem, then 1rem at
   max-width:1024px) still didn't work per user feedback: many iPads in landscape (Air/Pro etc.)
   are WIDER than 1024px (e.g. 1180/1194/1366px) and so fell completely outside the old
   max-width window. Now detected via touch capability instead (hover:none + pointer:coarse =
   finger instead of mouse/trackpad) — catches it reliably regardless of orientation/exact
   width. min-width:601px prevents overlap with the mobile rule above (there the desktop links
   are already hidden via display:none anyway). Plain desktop/laptop with a mouse (pointer:fine)
   stays unchanged at 0.86rem/17px. */
@media (min-width: 601px) and (hover: none) and (pointer: coarse) {
  .toolbar-home { font-size: 1rem; }
  .toolbar-home svg { width: 20px; height: 20px; }
  .toolbar-nav-link { font-size: 1rem; }
}

/* Lang/theme trigger: deliberately NOT a circle (small border-radius instead of 999px) —
   a rounded rectangle, the same shape for both, so they read as a matched pair. */
.lang-trigger {
  height: 34px;
  padding: 0 12px;
  border-radius: 9px;
  border: 1px solid var(--border);
  background: var(--card-bg);
  box-shadow: var(--shadow);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text);
  font-size: 0.76rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  cursor: pointer;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.lang-trigger:hover { transform: scale(1.05); }
.lang-trigger:active { transform: scale(0.94); }
/* Only the trigger IN the header bar gets overridden, not the .theme-trigger base class
   itself — #themeTrigger here shares that class with the floating funnel button
   (#jumpToFilterBtn), which per spec must stay unchanged (44px, round). Scoped via the ID, the
   funnel button stays completely unaffected by this. */
#siteToolbar .theme-trigger {
  width: auto;
  height: 34px;
  padding: 0 10px;
  border-radius: 9px;
  background: var(--card-bg);
  backdrop-filter: none;
}
#siteToolbar .theme-trigger svg { width: 17px; height: 17px; }

/* Both selection lists (Language/Theme) sit as their own block DIRECTLY below .toolbar-bar
   instead of as a sibling of their respective trigger (see the HTML comment on .toolbar-bar) —
   opening them grows .site-toolbar-inner itself (and with it the whole header bar including
   background/blur/border) downward, without touching the fixed .toolbar-bar row. */
.toolbar-menu {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 6px;
  max-height: 0;
  overflow: hidden;
  opacity: 0;
  margin-top: 0;
  transition: max-height 0.22s ease, opacity 0.18s ease, margin-top 0.22s ease;
}
.toolbar-menu.open {
  max-height: 200px;
  opacity: 1;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--section-divider);
}
.toolbar-menu .lang-btn, .theme-option {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 7px 12px;
  border-radius: 9px;
  border: 1px solid var(--border);
  background: var(--card-bg);
  color: var(--text-muted);
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease, transform 0.15s ease, border-color 0.15s ease;
}
.toolbar-menu .lang-btn:hover, .theme-option:hover { background: var(--row-hover); color: var(--text); }
.toolbar-menu .lang-btn:active, .theme-option:active { transform: scale(0.96); }
.toolbar-menu .lang-btn.active, .theme-option.active { background: var(--accent-bg); color: var(--accent-dark); border-color: var(--accent); }
.theme-option svg { width: 15px; height: 15px; flex-shrink: 0; }

/* Shows only the SVG for the SELECTED theme state (not the resolved appearance of "System")
   in the trigger — the same hidden/visible toggling as .jump-icon-filter/.jump-icon-add on
   #jumpToFilterBtn, driven here by updateThemeTriggerIcon() in app.js. */
.theme-trigger-icon { display: none; }
.theme-trigger-icon.active { display: block; }

.section-hero {
  position: relative;
  overflow: hidden;
  background: var(--section-hero-grad);
  padding: 44px 0 48px;
}
.section-filter { background: var(--section-filter-grad); }
.section-results { background: var(--section-results-grad); }
.section-sources { background: var(--section-sources-grad); }

@keyframes heroFadeUp {
  from { opacity: 0; transform: translateY(18px); }
  to { opacity: 1; transform: translateY(0); }
}
.hero-fade { opacity: 0; animation: heroFadeUp 0.75s ease forwards; }

.scroll-reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}
.scroll-reveal.revealed { opacity: 1; transform: translateY(0); }
@media (prefers-reduced-motion: reduce) {
  .hero-fade, .scroll-reveal { opacity: 1; transform: none; animation: none; transition: none; }
}
/* Illustration as a full-width background layer behind the centered text (Layer 6, Part B) —
   the same file, just repositioned instead of living in its own grid column. Dimmed (opacity)
   so the white text above it stays readable; an additional radial veil in the middle gives the
   text a bit more contrast headroom without altering the image itself. */
.hero-art-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  overflow: hidden;
}
.hero-art-bg::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse 60% 70% at 50% 45%, rgba(4, 20, 33, 0.32), transparent 72%);
}
/* aspect-ratio + object-fit instead of plain "width:100%, height:auto": without a width/height
   attribute on the <svg> itself, the intrinsic size browsers assume is inconsistent (Safari
   especially), which could crop the illustration on the sides depending on the device, even
   though the width was mathematically 100% of its already full-width container. A hardwired
   aspect ratio (= viewBox 1000×700) guarantees the complete illustration ALWAYS stays visible.
   max-width is now tied to --strip-max/--page-gutter (= 1152px) instead of a fixed 900px: that
   is exactly the content width of .section-inner, where the 5 preset cards also sit — on wide
   screens the illustration therefore reaches all the way under the last card ("Bilingual
   Schools") instead of stopping already at the third one. Deliberately NOT all the way to the
   viewport edge (that was tried and reverted: on very wide monitors the buildings looked too
   large/blocky) — tying it to --strip-max caps the illustration at the same width as the rest
   of the page content, so it never grows past the presets. */
.hero-city-img { width: 100%; max-width: calc(var(--strip-max) - 2 * var(--page-gutter)); height: auto; aspect-ratio: 1000 / 700; object-fit: contain; display: block; opacity: 0.3; }
.hero-copy-centered {
  position: relative;
  z-index: 1;
  max-width: 920px;
  margin: 0 auto;
  text-align: center;
}
.hero-copy-centered h1 {
  font-family: 'Manrope', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: clamp(2.1rem, 4.4vw, 3.1rem);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -0.02em;
  margin: 0 0 14px;
  color: #ffffff;
  text-shadow: 0 2px 16px rgba(3, 20, 35, 0.35);
}
/* Each line is its own data-i18n block instead of letting it wrap freely: "Compare the Facts."
   stays line 1, "Choose the Right School with Confidence." line 2 — independent of the natural
   text flow, which used to wrap onto 3 lines at 640px container width. nowrap keeps each line
   in one piece on desktop as much as possible; below 600px (see the media query below) it's
   allowed to wrap after all. */
.hero-headline-line { display: block; white-space: nowrap; }
.hero-subtitle {
  font-size: 1.05rem;
  line-height: 1.6;
  color: rgba(255, 255, 255, 0.92);
  margin: 0 auto;
  text-shadow: 0 1px 10px rgba(3, 20, 35, 0.3);
  white-space: nowrap;
}
/* Quick preset cards (Layer 7): five equal-width cards, a grid on desktop, a horizontally
   scrolling row on mobile (see the media query below) — each card sets the matching Browse
   filter state before navigating (activatePreset() in app.js), Bilingual stays a disabled/
   unclickable <span> with a "Soon" badge. */
.hero-presets {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 12px;
  margin-top: 90px;
}
.preset-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 16px 10px;
  border-radius: 14px;
  /* Higher opacity (was 0.12) — the crane/building illustration in the hero background used
     to show through noticeably and made the cards look flat/translucent; same hue, just more
     solid, so elevation/shadow stays visible and icon+title stand out clearly. */
  background: rgba(255, 255, 255, 0.26);
  border: 1px solid rgba(255, 255, 255, 0.22);
  backdrop-filter: blur(6px);
  text-align: center;
  text-decoration: none;
  color: #ffffff;
  cursor: pointer;
  transition: transform 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
}
.preset-card:hover { transform: translateY(-3px); background: rgba(255, 255, 255, 0.36); border-color: rgba(255, 255, 255, 0.4); }
.preset-card:active { transform: scale(0.97); }
.preset-card-icon { width: 22px; height: 22px; flex-shrink: 0; }
.preset-card-icon svg { width: 100%; height: 100%; }
.preset-card-label { font-size: 0.82rem; font-weight: 700; line-height: 1.25; }
/* Bilingual: deliberately not an <a>, no hover lift, dimmed look + a fixed "Soon" chip instead
   of a click target — signals "exists, but not usable yet" instead of just being missing. */
.preset-card-disabled {
  opacity: 0.5;
  cursor: default;
  pointer-events: none;
}
.preset-card-soon {
  font-size: 0.62rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 2px 8px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.22);
}
@media (max-width: 600px) {
  /* Peek carousel instead of a grid: the last visible card is deliberately cut off, so it's
     clear there's more to scroll (user requirement — NEVER let a card end exactly flush with
     the right edge). -webkit-overflow-scrolling for smooth iOS scrolling. Deliberately NO
     scroll-snap (anymore) — user requirement: the row should run freely while swiping, the
     user should be able to let go wherever they want, without the system snapping/centering
     on a card. */
  .hero-presets {
    display: flex;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    gap: 10px;
    /* = mobile .stat-row margin-top (50px) further down: A→B and B→C are the same size, three
       clear groups like in the desktop comment on .hero-cta-row. The cluster's inner spacing
       (Presets↔CTA, .hero-cta-row margin-top on mobile) deliberately stays unchanged at 14px. */
    margin: 50px calc(var(--page-gutter) * -1) 0;
    padding: 0 var(--page-gutter);
    scrollbar-width: none;
  }
  .hero-presets::-webkit-scrollbar { display: none; }
  .preset-card {
    flex: 0 0 auto;
    width: 40vw;
    max-width: 168px;
  }
}

/* Both CTAs deliberately share the same class/look (user requirement: equal weight, no
   primary/secondary distinction) — the difference is only the link target, not the style. */
.hero-cta-row {
  position: relative;
  z-index: 1;
  display: flex;
  justify-content: center;
  gap: 14px;
  /* Presets + CTA deliberately form ONE cluster (user requirement) — the break sits before
     the presets (A→B) and after the CTAs (B→C), see .hero-presets / .stat-row margin-top;
     both set to the same value (56px) so the two inter-group gaps are equal in size. */
  margin-top: 18px;
}
/* Layer 9: the main CTAs (Explore/Compare) carry the primary blue instead of the secondary
   teal — per the spec, both are "primary" actions. */
.btn-cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 13px 28px;
  border-radius: 999px;
  background: #ffffff;
  color: var(--primary);
  font-weight: 700;
  font-size: 0.95rem;
  text-decoration: none;
  border: 1px solid transparent;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
  box-shadow: 0 4px 16px rgba(3, 20, 35, 0.18);
}
.btn-cta:hover { transform: translateY(-2px); box-shadow: 0 8px 22px rgba(3, 20, 35, 0.24); }
.btn-cta:active { transform: scale(0.97); }
@media (max-width: 600px) {
  /* Side by side instead of stacked, but each button may only take up half the available
     width (flex:1) — otherwise a long label would push the other button out of the row. */
  /* Presets + CTA should read as ONE "cluster" on mobile (user requirement: barely any gap
     between them) — the clear break instead comes AFTER the CTAs, before Stats (see
     .stat-row margin-top further down). */
  .hero-cta-row { gap: 10px; margin-top: 14px; }
  .btn-cta { flex: 1 1 0; padding: 12px 14px; font-size: 0.88rem; }
}

/* Layer 7: deliberately boxless (no background/radius/border) — plain number+label cells,
   separated by thin divider lines instead of card edges, so they clearly stand apart from the
   preset cards and don't compete with their visual lead. */
.stat-row {
  position: relative;
  z-index: 1;
  display: flex;
  gap: 0;
  /* = .hero-presets margin-top (90px): A→B and B→C should be equal in size (three clear
     groups: header+subheader / presets+CTA cluster / stats), see the comment on .hero-cta-row. */
  margin-top: 90px;
}
.stat-card {
  position: relative;
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 4px 18px;
}
.stat-card + .stat-card { border-left: 1px solid rgba(255, 255, 255, 0.3); }
.stat-number { font-family: 'Manrope', inherit; font-size: 1.6rem; font-weight: 600; color: rgba(255, 255, 255, 0.85); line-height: 1.15; }
.stat-label { font-size: 0.72rem; font-weight: 500; color: rgba(255, 255, 255, 0.65); margin-top: 4px; }
/* Mobile: not a 2×2 grid, but 1 (Schools Compared, full width) over 3 (Gymnasium/
   Stadtteilschule/districts side by side) — a 3-column grid, first cell spans all three.
   First reset .stat-card border-left/-top (otherwise the desktop rule ".stat-card +
   .stat-card" would still apply), then, deliberately: a thin horizontal line across the whole
   bottom row (border-top on every cell except the first). The vertical divider lines between
   the three bottom cells are deliberately NOT a full border-left (that would touch/merge with
   the horizontal line above) — instead a short, vertically centered ::before stroke (a
   "|"-like mark) that floats freely between the top and bottom cell edge. */
@media (max-width: 600px) {
  .stat-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0; }
  .stat-card:first-child { grid-column: 1 / -1; }
  .stat-card:not(:first-child) { border-top: 1px solid rgba(255, 255, 255, 0.3); }
  /* The specificity of ".stat-card + .stat-card" (the desktop rule above) is exactly as high
     as ":nth-child" — as the later rule in cascade order it wins on a tie, so it's explicitly
     reset here on the second cell (the first of the bottom row of three). */
  .stat-card:nth-child(2) { border-left: none; }
  .stat-card:nth-child(3), .stat-card:nth-child(4) { border-left: none; }
  .stat-card:nth-child(3)::before, .stat-card:nth-child(4)::before {
    content: '';
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
    width: 1px;
    height: 18px;
    background: rgba(255, 255, 255, 0.35);
  }
}

.subtitle { color: var(--text-muted); font-size: 0.95rem; margin: 0 0 20px; line-height: 1.5; }
/* The fine hatching from the earlier cards is kept as the section's texture — but now edge-to-
   edge across the full width instead of filling a floating box. No overflow:hidden needed,
   because nothing can stick out past a card edge anymore (ALIGNMENT.md §6). */
.section-filter::after,
.section-results::after,
.section-sources::after {
  content: '';
  position: absolute;
  z-index: 0;
  inset: 0;
  background-image: repeating-linear-gradient(135deg, var(--border) 0, var(--border) 1px, transparent 1px, transparent 26px);
  opacity: 0.05;
  pointer-events: none;
}
.section-inner { position: relative; z-index: 1; }
.section-title {
  display: flex;
  align-items: center;
  /* 8px instead of the usual 12px: icon-chip (34px) + 8px = 42px = --content-inset, so the
     title text starts at exactly the same spot as the text after the step badge (28px + 14px
     gap = also 42px) and every content row below it. See ALIGNMENT.md rule 1. */
  gap: 8px;
  font-family: 'Manrope', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: 1.2rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin: 0 0 6px;
  color: var(--primary);
}
.section-title + .subtitle { margin-top: 4px; }
/* Neutral instead of teal (user requirement): the marker before section titles is
   structural/orienting ("a new area starts here"), not an action — teal stays reserved for
   click/selection meaning. A light framed chip with a dark outline instead of a gradient tile. */
.icon-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 34px;
  height: 34px;
  border-radius: 11px;
  background: var(--header-bg);
  border: 1px solid var(--border);
  color: var(--text);
}
.icon-chip svg { width: 18px; height: 18px; }
.controls {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  margin-top: 20px;
  align-items: center;
  /* .controls sits outside the numbered steps and so has no automatic left inset from
     badge+gap like .filter-step-body — --content-inset matches that up with the step titles on
     the left and mirrors the same spacing on the right, so the search field/counter/reset
     button as a group sit exactly between the two reference points. */
  padding-left: var(--content-inset);
  padding-right: var(--content-inset);
}
/* Semi-bold like the 💡 hint banner in the results card (.view-hint-banner, font-weight: 600)
   — both are hints with the same role and should stand out equally. */
#addressHint { padding-right: var(--content-inset); font-weight: 600; }
.controls .autocomplete-wrap { flex: 1 1 240px; }
.controls input[type="search"] {
  width: 100%;
  box-sizing: border-box;
  padding: 10px 14px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  font-size: 0.95rem;
  transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.controls select {
  padding: 10px 14px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  font-size: 0.95rem;
  transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.controls input[type="search"]:focus,
.controls select:focus,
.search-row input[type="text"]:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-bg);
}
.result-count {
  color: var(--text-muted);
  font-size: 0.85rem;
  white-space: nowrap;
}
/* Host for the edge shadows (ALIGNMENT.md §9): sits OUTSIDE the scroll container so the
   shadows stay still while scrolling. Identical to .compare-tab-panel in the comparison
   modal/on the Compare page — same treatment for the same overflow. */
/* The outer margin sits on the host, not on the scroller: otherwise the host's top edge would
   sit 16px above the table and the shadows would start in empty space. */
.table-scroll-host { position: relative; margin-top: 16px; }
.table-scroll-host.is-overflowing::before,
.table-scroll-host.is-overflowing::after {
  content: '';
  position: absolute;
  top: 1px;
  bottom: 1px;
  width: 26px;
  pointer-events: none;
  z-index: 3;
}
.table-scroll-host.is-overflowing::before {
  left: 1px;
  border-radius: 12px 0 0 12px;
  background: linear-gradient(to right, var(--card-bg), transparent);
  opacity: var(--compare-fade-start, 0);
}
.table-scroll-host.is-overflowing::after {
  right: 1px;
  border-radius: 0 12px 12px 0;
  background: linear-gradient(to left, var(--card-bg), transparent);
  opacity: var(--compare-fade-end, 1);
}
.table-scroll {
  /* A fixed height instead of a 6000px-tall table: only the body scrolls, the header row stays
     put via position:sticky (that rule already lives on thead th, it just didn't take effect
     before because there was no bounded scroll container). This value is just the starting
     value/mobile limit (no sidebar counterpart there, it's an overlay sheet) — from 900px up,
     syncTableHeightToSidebar() in app.js sets this value via inline style to the sidebar's
     actual height (user requirement: "the table box's height should be tied to the filter
     box's"). Plain CSS (grid stretch + flex min-height:0) was tried but didn't work:
     .browse-layout is itself only auto-height (no fixed height), so for the row height in grid
     tracking, the table's full content size counts, not the stretched box — without an
     external, fixed reference height this stays a pure measurement problem that only JS can
     solve. */
  max-height: min(70vh, 720px);
  overflow-y: auto;
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--card-bg);
  /* EXCEPTION to the content inset (ALIGNMENT.md §5): the table starts at the left edge of the
     step badge or the card title icon — i.e. the edge of the card's inner area — and ends on
     the right mirroring the same spacing. Its 10 columns are the only spot where the 84px of
     inset isn't cosmetic, but decides whether it can be read at all without horizontal
     scrolling. The other content rows (counter pill, quick access, toggle, cards/map) keep the
     inset. */
  margin-left: 0;
  margin-right: 0;
  transition: opacity 0.25s ease;
}
#schoolTable {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.88rem;
  min-width: 920px;
  table-layout: auto;
}
thead th {
  background: var(--header-bg);
  text-align: left;
  vertical-align: bottom;
  padding: 10px 7px;
  font-size: 0.83rem;
  cursor: pointer;
  user-select: none;
  white-space: normal;
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  /* Without this: .sozial-badge (position:relative, for its tooltip) sits in the DOM AFTER
     <thead>, and both otherwise have z-index:auto — at the same stacking level, the later DOM
     position wins, so the badge row would paint itself over the supposedly-fixed column header
     while scrolling instead of disappearing beneath it (visible as "showing through" values in
     the Sozialindex header). Any positive value would do, 2 deliberately stays below the edge
     shadow's z-index (3, see .table-scroll-host.is-overflowing::before/::after), so its fade
     effect at the header row's corners still sits above it as before. */
  z-index: 2;
  font-weight: 600;
  letter-spacing: 0.01em;
  line-height: 1.3;
  transition: color 0.15s ease;
}
thead th:hover { color: var(--accent-dark); }
/* Thanks to vertical-align:bottom on the <th> itself, .th-inner sits flush with the BOTTOM edge
   of the header row (stretched together to the tallest cell) — a uniform height in every
   column, regardless of whether the title wraps to one or two lines. */
.th-inner { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.th-title-row { display: inline-flex; align-items: center; gap: 3px; }
thead th .arrow {
  display: inline-block;
  color: var(--accent);
  opacity: 0;
}
thead th.sorted .arrow { opacity: 1; }
tbody td {
  padding: 9px 7px;
  border-bottom: 1px solid var(--border);
}
tbody tr { cursor: pointer; transition: background-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease; }
tbody tr:hover { background: var(--row-hover); box-shadow: inset 3px 0 0 var(--accent); transform: translateY(-1px); }
tbody tr:last-child td { border-bottom: none; }
.badge {
  display: inline-block;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 600;
  background: var(--accent-bg);
  color: var(--accent-dark);
  white-space: nowrap;
}
/* Type color coding only on the card (user requirement, "just on cards for now") — the table
   and comparison modal keep the neutral .badge teal look unchanged, since it wasn't requested
   there. */
.badge-type-gymnasium { background: var(--type-gymnasium-bg); color: var(--type-gymnasium-text); }
.badge-type-stadtteilschule { background: var(--type-stadtteilschule-bg); color: var(--type-stadtteilschule-text); }
.grade-chip {
  display: inline-block;
  /* Counterpart to .trend-arrow: both set to middle so the chip and arrow sit on the same
     center line instead of the baseline (see the comment there). */
  vertical-align: middle;
  padding: 3px 10px;
  border-radius: 999px;
  border: 1px solid transparent;
  font-family: 'Manrope', inherit;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.dist-dots { display: inline-flex; gap: 2px; vertical-align: middle; margin-right: 5px; }
.dist-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
/* Only the distance table cell: dots always above the km value instead of side by side inline
   (prevents the previous line-wrap lottery depending on cell width/digit count), both together
   as one block, horizontally centered. Dot size/spacing scaled up in the same 3:1 ratio as
   before (6px/2px) so it doesn't look cramped — the same scale as .distance-inline below (card
   view), just that there dots+value stay side by side inline. */
.distance-cell { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.distance-cell .dist-dots { gap: 3px; margin-right: 0; }
.distance-cell .dist-dot { width: 9px; height: 9px; }
/* Card view: the layout (label + dots + km value on one line) stays unchanged, only the dots
   get scaled to the same size as in the table cell. */
.distance-inline .dist-dots { gap: 3px; }
.distance-inline .dist-dot { width: 9px; height: 9px; }
/* Most Preferred % cell: a chip (percentage, colored by tier) above a small Low/Medium/High
   label, same column layout as .distance-cell. */
.demand-cell { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.demand-band-label { font-size: 0.68rem; font-weight: 700; letter-spacing: 0.02em; color: var(--text-muted); }
.num { text-align: center; font-variant-numeric: tabular-nums; }
/* A value and its trend indicator (▲▼▶◎) must NEVER wrap onto two lines (ALIGNMENT.md §6).
   You can't rely on the available column width for this: whether a wrap point even forms
   between the number and the arrow depends on the glyph's line-break class and the platform's
   font metrics — it wraps on iOS/Safari, not on Chromium/Linux. nowrap removes the wrap point
   regardless. If there isn't enough space, the column gets wider or the table scrolls
   (.table-scroll), instead of tearing the row apart.
   The distance cell is unaffected by this: its two lines come from .distance-cell's flex
   column layout, not from a line wrap. */
#schoolTable td.num { white-space: nowrap; }
.col-center { text-align: center; }
.na { color: var(--text-muted); }
/* Rank-among-all-schools row in the comparison modal (Academics tab): the band bold/normal on
   top, "#rank of total schools" small/muted below — the same structure as the other two-line
   cells in this table (large label, small context). */
.rank-band { display: flex; flex-direction: column; gap: 2px; }
.rank-band-label { font-weight: 700; color: var(--text); }
.rank-band-sub { font-size: 0.78rem; color: var(--text-muted); }
.empty-state {
  text-align: center;
  padding: 40px;
  color: var(--text-muted);
}
footer {
  /* No more margin-top: the spacing comes from its own padding, so the footer sits flush
     against the last section instead of showing a gap in the page background. */
  padding: 28px var(--page-gutter) 40px;
  max-width: var(--strip-max);
  margin: 0 auto;
  color: var(--text-muted);
  font-size: 0.8rem;
  text-align: center;
}
#map {
  height: 480px;
  width: 100%;
  border-radius: 12px;
  border: 1px solid var(--border);
  margin-top: 14px;
  background: var(--bg);
}
.leaflet-interactive { filter: drop-shadow(0 1px 3px rgba(15, 23, 42, 0.35)); }
.leaflet-popup-content-wrapper { border-radius: 10px; box-shadow: var(--shadow); }
.leaflet-popup-content { font-family: inherit; font-size: 0.88rem; line-height: 1.55; margin: 12px 14px; }
.leaflet-popup-content strong { color: var(--accent-dark); }
.popup-table { border-collapse: collapse; margin-top: 6px; }
.popup-table td { padding: 2px 0; font-size: 0.85rem; }
.popup-table td:first-child { color: var(--text-muted); padding-right: 12px; white-space: nowrap; }
.popup-table td:last-child { font-weight: 600; text-align: right; }
.popup-view-school {
  display: block;
  width: 100%;
  margin-top: 10px;
  padding: 6px 10px;
  border: none;
  border-radius: 8px;
  background: var(--accent);
  color: var(--accent-contrast);
  font-size: 0.82rem;
  font-weight: 600;
  cursor: pointer;
  transition: filter 0.15s ease;
}
.popup-view-school:hover { filter: brightness(1.08); }
.leaflet-tooltip {
  background: var(--card-bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: var(--shadow);
  font-family: inherit;
  font-size: 0.8rem;
  line-height: 1.45;
  padding: 6px 10px;
}
.leaflet-tooltip-top::before { border-top-color: var(--card-bg); }
.leaflet-tooltip-bottom::before { border-bottom-color: var(--card-bg); }
.leaflet-tooltip-left::before { border-left-color: var(--card-bg); }
.leaflet-tooltip-right::before { border-right-color: var(--card-bg); }
:root[data-theme="dark"] .leaflet-popup-content-wrapper,
:root[data-theme="dark"] .leaflet-popup-tip { background: var(--card-bg); color: var(--text); }
:root[data-theme="dark"] .leaflet-bar a { background: var(--card-bg); color: var(--text); border-color: var(--border); }
:root[data-theme="dark"] .leaflet-bar { border-color: var(--border); }
@media (prefers-color-scheme: dark) {
  .leaflet-popup-content-wrapper, .leaflet-popup-tip { background: var(--card-bg); color: var(--text); }
  .leaflet-bar a { background: var(--card-bg); color: var(--text); border-color: var(--border); }
  .leaflet-bar { border-color: var(--border); }
}
.search-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 20px;
  /* No more --content-inset right inset (user requirement): was left over from the old
     full-width page and made the address field in the sidebar visibly end shorter than the
     school search above it, even though both should have the same column width. */
}
.radius-filter-row {
  display: flex;
  flex-direction: column;
  gap: 7px;
  /* Same spacing between two control blocks as in step 3 (.adv-filters-body gap), so the
     address field and radius don't stick together. */
  margin-top: 20px;
  /* No more right inset (user requirement, same as .adv-filters-body above): the radius block
     should reach the sidebar's right edge like District/Sozialindex/Grade etc. do. */
}
.radius-filter-row > label {
  font-size: 0.76rem;
  font-weight: 700;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.radius-filter-row .dual-range-input:disabled { cursor: not-allowed; }
/* opacity on .dual-range itself instead of just on track/fill, so the native thumb
   (::-webkit-slider-thumb/::-moz-range-thumb) visibly dims along with it — otherwise the
   handle would stay at full accent color while only the line beneath it looked faded. */
.radius-filter-row.disabled .dual-range,
.radius-filter-row.disabled .adv-range-value { opacity: 0.45; }
.radius-filter-row.disabled > label { opacity: 0.6; }
.search-row .autocomplete-wrap { flex: 1 1 280px; }
.search-row input[type="text"] {
  width: 100%;
  box-sizing: border-box;
  padding: 10px 14px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  font-size: 0.95rem;
  transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.autocomplete-wrap { position: relative; }
/* Confirms the address was really resolved: sits at the right end of the address field instead
   of as its own text line below — that line used to repeat the address, which is already right
   there in the field above it (ALIGNMENT.md §4). Accent color and a bold stroke weight like the
   dropdown arrow, so it's recognizable at a glance. */
.address-ok-check {
  position: absolute;
  right: 14px;
  top: 50%;
  transform: translateY(-50%);
  width: 20px;
  height: 20px;
  color: var(--accent);
  pointer-events: none;
}
/* Keep space clear so a long address doesn't run under the checkmark. */
#addressSearch.has-check { padding-right: 42px; }
/* An "X" to clear ONLY the address on purpose (user requirement) — sits to the left of the
   checkmark, in the same spot as the checkmark itself WHEN no checkmark is visible (just text,
   no resolved address yet); slides left as soon as the checkmark appears. */
.address-clear-btn {
  position: absolute;
  right: 14px;
  top: 50%;
  transform: translateY(-50%);
  /* A bit larger than before (18px) and matched to the checkmark (20px) (user requirement: the
     two icons looked inconsistently sized) — plus coral as a signal color, so it's clear at a
     glance that this is a delete action, not a second confirmation next to the green
     checkmark. */
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  background: none;
  color: var(--coral);
  cursor: pointer;
}
.address-clear-btn:hover { color: var(--coral-hover); }
.address-clear-btn svg { width: 16px; height: 16px; }
#addressSearch.has-check ~ .address-clear-btn { right: 40px; }
#addressSearch.has-clear { padding-right: 40px; }
#addressSearch.has-clear.has-check { padding-right: 68px; }
/* Like .ms-panel: position:fixed + coordinates from JS, and attached to <body> while open —
   otherwise .card's overflow:hidden would clip the list at the card's bottom edge (measured:
   76px for the address list). See positionFloatingPanel(). */
.autocomplete-list {
  position: fixed;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow-hover);
  max-height: 260px;
  overflow-y: auto;
  /* Above the mobile filter sheet overlay (.browse-sidebar, z-index:400) — otherwise the list
     attached to <body> renders invisibly BEHIND the sheet when the address field/school search
     lives inside it (observed on mobile: address suggestions didn't appear). */
  z-index: 420;
}
.autocomplete-item {
  padding: 9px 14px;
  font-size: 0.9rem;
  color: var(--text);
  cursor: pointer;
}
.autocomplete-item:hover, .autocomplete-item.active-suggestion { background: var(--accent-bg); }
.autocomplete-empty {
  padding: 9px 14px;
  font-size: 0.85rem;
  color: var(--text-muted);
}
.search-status {
  color: var(--text-muted);
  font-size: 0.85rem;
  margin-top: 10px;
}
/* No more min-height that would have permanently reserved an empty row: in the normal case (no
   address searched), step 2 now ends cleanly after the radius slider, instead of pushing step 3
   down against an invisible row. */
.search-status:empty { display: none; }
.legend {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 14px;
  font-size: 0.8rem;
  color: var(--text-muted);
}
.legend .swatch {
  display: inline-block;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  margin-right: 3px;
  vertical-align: middle;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15);
}
.hidden { display: none !important; }
.distance-col { display: none; }
#schoolTable.show-distance .distance-col { display: table-cell; }
/* Layer 9: utility/quick-action buttons (Nearest Schools, Best Grades, Load More, Distance
   View) are "secondary" actions like Sort/Reset → coral, instead of the neutral default color. */
.btn-secondary {
  padding: 10px 20px;
  border-radius: 999px;
  border: 1px solid var(--coral);
  background: var(--bg);
  /* var(--coral) instead of --coral-text: this is a ghost/outline button (the background stays
     the page color, only border+text are coral) — --coral-text is meant exclusively for text ON
     a solid coral background (dark text needs a dark contrast partner). In the dark theme,
     --bg (#0b1622) and --coral-text (#0f172a) sit almost exactly on top of each other, which
     made the button text completely invisible (user screenshot: "Reset"/"X more" unreadable). */
  color: var(--coral);
  font-size: 0.88rem;
  font-weight: 600;
  cursor: pointer;
  box-shadow: var(--shadow);
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
.btn-secondary:hover { background: rgba(242, 115, 92, 0.12); transform: scale(1.04) translateY(-1px); }
.btn-secondary:active { transform: scale(0.96); }
.btn-secondary.active {
  border-color: var(--coral);
  color: var(--coral);
  background: rgba(242, 115, 92, 0.18);
}
.map-actions {
  margin-top: 14px;
}
.sozial-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  position: relative;
  cursor: pointer;
}
/* Shared trend-arrow look for Average Grade AND Sozialindex — identical everywhere (table,
   cards, card popups, comparison modal): bold, noticeably larger than normal text, green/red/
   gray depending on direction instead of the earlier thin Unicode arrows. */
.trend-arrow {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  line-height: 1;
  margin-left: 3px;
  cursor: default;
  /* Centered against the value next to it: the chip is taller than the text line because of
     its inner padding, so aligned to the baseline the arrow would otherwise sit about 3px too
     high. With BOTH set to vertical-align:middle, their center points line up (see
     .grade-chip). */
  vertical-align: middle;
}
/* Size comes from em, so it follows the surrounding font size (table, card, popup, legend)
   exactly like the earlier glyph did. font-weight/font-size are dropped — for an SVG the
   geometry controls the weight, not the font. */
.trend-arrow-glyph { width: 1.35em; height: 1.35em; display: block; }
/* Neutral arrow (Layer 5.7, Sozialindex + application counts): a fixed teal color instead of
   the green/red/gray judgment from trendArrowHtml() — the direction (up/down/same) stays
   visible via the arrow's rotation, but without the "good" or "bad" undertone that green/red
   would wrongly suggest here. */
.trend-arrow-neutral { color: var(--accent-dark); }
/* Guards the value+arrow pair against line wrapping AND centers both vertically relative to
   each other, everywhere there isn't already a dedicated inline-flex wrapper
   (.sozial-badge/.compare-stat-value already have one) — specifically the card-view popup/
   tooltip and the "Current Applications" row. DELIBERATELY inline-flex + align-items:center
   itself instead of just white-space:nowrap: measured, the arrow otherwise sat about 1.3px too
   low (vertical-align:middle in normal text flow aligns to the line box, not the flexbox center
   axis — .sozial-badge/.compare-stat-value land at exactly 0px difference because they were
   ALWAYS already flex containers). Related to ALIGNMENT.md §9 (whitespace-free chaining alone
   isn't always enough on iOS/Safari). */
.value-trend-pair { display: inline-flex; align-items: center; white-space: nowrap; }
/* Fills exactly the space the trend arrow would otherwise take up when there isn't enough
   history (ALIGNMENT.md rule 6) — otherwise the cell/card would look unfilled compared to rows
   with an arrow. Explains via tooltip WHY there's no arrow, instead of just showing nothing. */
.trend-nodata-icon {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 13px;
  height: 13px;
  margin-left: 3px;
  vertical-align: middle;
  border-radius: 50%;
  border: 1px solid var(--border);
  color: var(--text-muted);
  opacity: 0.75;
  cursor: pointer;
  flex-shrink: 0;
}
.trend-nodata-icon:hover { opacity: 1; border-color: var(--accent-light); color: var(--accent-dark); }
.trend-nodata-icon svg { width: 7px; height: 7px; }
/* Legend variant: the same glyph, but not interactive — the explanation is in the text there.
   Color/border from currentColor instead of a fixed theme variable, because the same legend
   appears both on a light background (Explanations modal, hint banner) and in the dark
   column-info popup — with var(--text-muted) it would be nearly invisible there. */
.trend-nodata-static {
  cursor: default;
  color: inherit;
  border-color: currentColor;
  opacity: 0.8;
}
.trend-nodata-static:hover { opacity: 0.8; border-color: currentColor; color: inherit; }
.trend-nodata-tooltip {
  position: absolute;
  bottom: 130%;
  left: 50%;
  transform: translateX(-50%);
  background: var(--text);
  color: var(--card-bg);
  padding: 5px 9px;
  border-radius: 8px;
  font-size: 0.72rem;
  font-weight: 500;
  /* Wraps instead of nowrap + max-width (indirectly a user requirement: this was the ~44px
     overlap that made html { overflow-x: clip } necessary above) — with nowrap the box could
     be up to 246px wide depending on language and would run past the viewport on a card at the
     right edge, far more than the 13px-wide speech-bubble tip justifies. */
  white-space: normal;
  max-width: 150px;
  text-align: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s ease;
  z-index: 30;
  box-shadow: var(--shadow);
}
.trend-nodata-icon:hover .trend-nodata-tooltip,
.trend-nodata-icon.tooltip-open .trend-nodata-tooltip {
  opacity: 1;
  pointer-events: auto;
}
.sozial-tooltip {
  position: absolute;
  bottom: 130%;
  right: 0;
  background: var(--text);
  color: var(--card-bg);
  padding: 6px 10px;
  border-radius: 8px;
  font-size: 0.72rem;
  font-weight: 500;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s ease;
  z-index: 30;
  box-shadow: var(--shadow);
}
.sozial-badge:hover .sozial-tooltip,
.sozial-badge.tooltip-open .sozial-tooltip {
  opacity: 1;
  pointer-events: auto;
}
.sources-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 8px;
}
.source-line {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  text-align: left;
  padding: 8px 6px;
  border: none;
  border-radius: 8px;
  background: none;
  color: var(--text-muted);
  font-size: 0.85rem;
  font-family: inherit;
  cursor: pointer;
  transition: color 0.15s ease, background-color 0.15s ease;
}
.source-line:hover { color: var(--accent-dark); background: var(--row-hover); }
.source-line svg { flex-shrink: 0; opacity: 0.55; transition: transform 0.15s ease; }
.source-line:hover svg { transform: translateX(2px); opacity: 0.9; }
.sources-details summary {
  cursor: pointer;
  list-style: none;
}
.sources-details summary::-webkit-details-marker { display: none; }
/* Reuses .step3-chevron (the Detailed Criteria arrow in the filter card) instead of its own
   ::after triangle — same size/weight as there, for the same reasons (too small, tied to the
   card edge via margin-left:auto instead of the title text). */
.sources-details[open] .step3-chevron { transform: rotate(90deg); }
.sources-details .sources-list { margin-top: 14px; }
#footerCredits { margin: 0; }
.footer-disclaimer {
  margin: 10px auto 0;
  max-width: 640px;
  font-size: 0.7rem;
  line-height: 1.5;
  color: var(--text-muted);
  opacity: 0.75;
}
.leaflet-control-center a {
  display: flex;
  align-items: center;
  justify-content: center;
}
.floating-actions {
  /* At the bottom instead of on the side (user requirement: like the compare pill) — used to
     be vertically centered on the right, which "floated" in the middle of the screen with a
     very long card list instead of lining up at the bottom like other floating actions. */
  position: fixed;
  right: 16px;
  bottom: 20px;
  z-index: 200;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}
/* Shifts upward while the compare pill (also bottom:20px) is visible — otherwise the button
   would sit right on/behind it. Same body class as the .browse-layout bottom reserve further
   down (see updateCompareBar() in app.js). */
body.has-compare-bar .floating-actions { bottom: 90px; }
/* .theme-trigger is the same class as #jumpToFilterBtn (the floating funnel button) — this
   base rule stays unchanged for it (44px, round). The header-bar variant is deliberately
   overridden further up via #siteToolbar .theme-trigger, see the comment there. */
.theme-trigger {
  width: 44px;
  height: 44px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--panel-bg);
  backdrop-filter: blur(8px);
  box-shadow: var(--shadow);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text);
  cursor: pointer;
  flex-shrink: 0;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.theme-trigger:hover { transform: scale(1.07); }
.theme-trigger:active { transform: scale(0.94); }
.theme-trigger svg { width: 20px; height: 20px; }

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

.type-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.type-and-fav-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.type-chip {
  padding: 8px 16px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text-muted);
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.15s ease;
}
.type-chip:hover { border-color: var(--accent-light); color: var(--text); transform: translateY(-1px); }
.type-chip:active { transform: scale(0.95); }
.type-chip.active {
  border-color: var(--accent);
  background: linear-gradient(135deg, var(--accent-light), var(--accent-dark));
  color: var(--accent-contrast);
  box-shadow: 0 4px 10px -4px rgba(13, 148, 136, 0.5);
}

.view-toggle {
  display: inline-flex;
  gap: 4px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 4px;
  /* No more margin-top: that was left over from the old layout, where .view-toggle sat
     directly below the results text. Now it's a flex child in .results-toolbar
     (align-items: center) — the margin-top pushed it down unnecessarily there and visibly
     shifted it below the sidebar's search input. */
}
.view-toggle button {
  padding: 7px 16px;
  border-radius: 999px;
  border: none;
  background: transparent;
  color: var(--text-muted);
  font-size: 0.82rem;
  font-weight: 700;
  cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease, transform 0.15s ease;
}
.view-toggle button:active { transform: scale(0.94); }
.view-toggle button.active {
  background: linear-gradient(135deg, var(--accent-light), var(--accent-dark));
  color: var(--accent-contrast);
}

th.compare-col { text-align: center; }
td.compare-col { width: 34px; text-align: center; }
th.rank-col, td.rank-col {
  width: 30px;
  text-align: center;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  font-size: 0.85rem;
}
.card-rank {
  position: absolute;
  top: -10px;
  left: -10px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  /* The same gradient duo as .type-chip.active/.view-toggle button.active instead of flat
     var(--accent) — consistent with the other secondary accents that carry a gradient. */
  background: linear-gradient(135deg, var(--accent-light), var(--accent-dark));
  color: var(--accent-contrast);
  font-size: 0.78rem;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow);
}
.compare-checkbox {
  width: 16px;
  height: 16px;
  accent-color: var(--accent);
  cursor: pointer;
  vertical-align: middle;
}
.compare-checkbox:disabled { cursor: not-allowed; opacity: 0.4; }

@keyframes checkPop {
  0% { transform: scale(1); }
  45% { transform: scale(1.35); }
  100% { transform: scale(1); }
}
.compare-checkbox:checked, .ms-option input:checked {
  animation: checkPop 0.3s ease;
}

.cards-grid {
  display: grid;
  /* One column at every width (user requirement): with 3 (and even 2) columns it was hard to
     cleanly align every data point on a card — as a full-width row throughout ("thin, long
     rectangle") each card has enough width for its stat boxes/meta rows. This also removes the
     earlier 900px/600px intermediate steps (2 or 1 columns) — there's now only this one case. */
  grid-template-columns: 1fr;
  gap: 16px;
  margin-top: 16px;
  transition: opacity 0.25s ease;
}
.school-card {
  position: relative;
  cursor: pointer;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 16px;
  box-shadow: var(--shadow);
  transition: transform 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease, border-color 0.18s ease;
  /* CSS grid tracks with fr units implicitly have min-width:auto, i.e. "never narrower than
     the widest unbreakable content" — a very long name with no wrap point (see
     .school-card-name: white-space:nowrap) would otherwise stretch the whole card column,
     instead of fitCardNames() shrinking it to fit within the card's intended width.
     min-width:0 here — not just on .school-card-name itself — removes that implicit lower
     bound from the grid track. */
  min-width: 0;
}
.school-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-hover); }
/* Visible feedback "this school is selected for comparison" on the card itself, not just on
   the small icon — the same teal tint as the active compare arrow (.card-compare-btn.active),
   so the card and icon speak the same language regardless of whether it was selected via mouse
   hover or touch. */
.school-card.is-compare-selected {
  background: var(--accent-bg);
  border-color: var(--accent);
}
.card-icon-group {
  position: absolute;
  bottom: -10px;
  right: -10px;
  display: flex;
  align-items: center;
  /* Neither gap nor padding here: EVERY pixel inside the pill must belong to one of the two
     buttons (their own padding now carries the visual spacing, see .card-icon-btn) — even a
     2px margin on the group container would be a clickable gap that belongs to no button and
     wrongly passes taps through to the card click. */
  gap: 0;
  padding: 0;
  border-radius: 999px;
  background: var(--card-bg);
  /* As a box-shadow instead of a border: a real border would eat 1px from the layout box that
     no button fills anymore — exactly the kind of 1px gap this whole change is meant to
     eliminate. box-shadow only paints the ring without including it in hit-testing. */
  box-shadow: 0 0 0 1px var(--border), var(--shadow);
  max-width: calc(100% - 12px);
}
/* The button fills its entire visible area via its own padding instead of group padding/gap —
   every pixel inside the pill genuinely belongs to the <button> (the click target) this way,
   there's no more "dead zone" that passes the tap through to the card click handler and
   accidentally opens the school profile instead of the selection. 8px padding + 17px icon give
   a ~33px area that's comfortable with a mouse; on pure touch devices (see @media
   (hover: none) further down) the padding grows to a genuine 44×44px WCAG minimum size. */
.card-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  flex-shrink: 0;
  border: none;
  background: none;
  padding: 8px;
  margin: 0;
  line-height: 1;
  cursor: pointer;
  color: var(--text-muted);
  max-width: 196px;
  transition: color 0.15s ease, background-color 0.15s ease;
}
.card-icon-btn svg { width: 17px; height: 17px; flex-shrink: 0; transition: transform 0.15s ease; }
.card-icon-btn:not(:disabled):hover svg, .card-icon-btn.expanded svg { transform: scale(1.1); }
.card-icon-btn:active { transform: scale(0.92); }
.card-icon-btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* Coarse pointer devices (a finger instead of a mouse cursor) get a noticeably larger hit
   target than the mouse variant above — the same (hover: none) detection that
   isTouchOnlyPointer() in app.js already uses for tap behavior, mirrored here for the touch
   target size. */
@media (hover: none) {
  .card-icon-btn { padding: 13px; }
  .card-icon-btn svg { width: 18px; height: 18px; }
}
.card-icon-label {
  display: inline-block;
  max-width: 0;
  overflow: hidden;
  white-space: nowrap;
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  opacity: 0;
  margin-right: 0;
  /* pointer-events:none: a click on the label should ALWAYS land on the surrounding <button>
     (the whole button is the click handler, see the .card-icon-btn listener) — this way a
     click can never "miss" onto a plain text element that doesn't trigger anything itself. */
  pointer-events: none;
  transition: max-width 0.25s ease, opacity 0.2s ease, margin-right 0.25s ease;
}
.card-icon-btn:not(:disabled):hover .card-icon-label,
.card-icon-btn.expanded .card-icon-label {
  max-width: 180px;
  opacity: 1;
  margin-right: 5px;
}
.card-compare-btn.active { color: var(--accent-dark); animation: checkPop 0.3s ease; }
.card-fav-btn.active { color: #ef4444; animation: checkPop 0.3s ease; }
/* The selected state must stand out clearly from the resting state — not just via a color
   change (which already looks "bold" on the heart because of its fill, but not on the compare
   arrow, since it's always just an outline). Both icons therefore get the SAME size rule in
   the active state, so their size balance relative to each other never drifts apart; only the
   compare arrow additionally gets a thicker outline, because unlike the heart it has no fill. */
.card-compare-btn.active svg,
.card-fav-btn.active svg {
  transform: scale(1.2);
}
.card-compare-btn.active svg { stroke-width: 3; }
.fav-filter-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 14px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text-muted);
  font-size: 0.82rem;
  font-weight: 600;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease, background-color 0.15s ease, transform 0.15s ease;
}
.fav-filter-toggle:hover { border-color: var(--accent-light); color: var(--text); }
.fav-filter-toggle:active { transform: scale(0.96); }
.fav-filter-toggle.active {
  border-color: #ef4444;
  background: rgba(239, 68, 68, 0.12);
  color: #dc2626;
}
/* Neutral instead of teal (user requirement): a hint text is information, not an action/
   selection. */
.view-hint-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 16px;
  /* No more --content-inset padding (user requirement, same as .cards-grid/.map-view/.results-
     toolbar above): this "tip" banner should have exactly the same width as .browse-main like
     the table/cards/map do, not narrower from an inset left over from the old full-width page. */
  padding: 10px 16px;
  border-radius: 12px;
  background: var(--header-bg);
  border: 1px solid var(--border);
  color: var(--text);
  font-size: 0.85rem;
  font-weight: 600;
}
.view-hint-close {
  border: none;
  background: transparent;
  color: var(--text-muted);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  flex-shrink: 0;
  padding: 0 2px;
}
/* The name row now also carries the type badge (user requirement: name on the left, Gymnasium/
   Stadtteilschule on the right, on ONE line — used to be its own row below). The achievement
   badge (Top X%) is still NOT here, it sits next to district/neighborhood in
   .school-card-location-row — see there. align-items:center instead of :flex-start, because now
   two children of different heights (the name text vs. the badge pill) sit side by side and
   should visually line up on the same row center. min-height stays fixed anyway:
   fitCardNames() can shrink the name to varying degrees depending on its length (11–16px),
   without a fixed value every card would get a different row height and shift every following
   element (location, metrics) down by a different amount card by card. The fixed value = the
   name's height at max font size (CARD_NAME_MAX_FONT_PX 16px × line-height 1.3 = 20.8px), the
   largest value a shrunk name never exceeds. */
.school-card-name-row {
  display: flex;
  align-items: center;
  gap: 8px;
  /* Kept small (user requirement): the name and location should read as more tightly connected
     than the card's other rows. */
  margin-bottom: 4px;
  min-height: 20.8px;
}
/* Fixed width (implicitly a user requirement via fitCardNames(), see the comment there): the
   badge must never shrink — only the name (flex:1 1 auto/min-width:0 below) gives way when
   space is tight, fitCardNames() then automatically measures the resulting reduced available
   name width. */
.school-card-name-row .badge { flex-shrink: 0; }
/* Always single-line, never wrap or get cut off (user requirement) — the actual shrinking is
   handled by fitCardNames() in app.js (sets style.fontSize directly on the element), these
   rules are only the CSS-side scaffolding for that: min-width:0 is what allows the flex item to
   shrink below its content width in the first place (otherwise JS couldn't meaningfully measure
   the available space). */
.school-card-name {
  font-size: 1rem;
  font-weight: 700;
  margin: 0;
  line-height: 1.3;
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
}
/* position:absolute (user requirement: "as if the badge were stuck on only AFTER the finished
   layout") — completely removed from .school-card-location-row's normal flow, so its
   height/text length ("Top 1%" vs. "Top 23%") can STRUCTURALLY never shift the row height or
   any element below it, regardless of font, browser, or column-width rounding. Vertically
   centered to the row via top:50%/translateY(-50%), right-aligned to the card's inner edge. */
.card-achievement-badge {
  position: absolute;
  top: 50%;
  right: 0;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 9px 3px 7px;
  border-radius: 999px;
  /* An experiment per user request: coral instead of gold (dedicated --achievement-badge-*
     tokens, see :root above) — separate from --achievement-bg/-text, which stays gold for the
     filter-count badge. */
  background: var(--achievement-badge-bg);
  color: var(--achievement-badge-text);
  font-size: 0.68rem;
  font-weight: 700;
  white-space: nowrap;
}
.card-achievement-badge svg { width: 12px; height: 12px; flex-shrink: 0; }
/* Average Grade + Graduates + Sozialindex as three EQUAL-WEIGHT blocks (user requirement): the
   same font size/weight for all three values, ALWAYS side by side on one row (mobile too) —
   smaller font/padding than the previous two blocks, so three boxes fit side by side without
   the row wrapping. A single label may wrap internally onto two lines (min-width:0 allows the
   shrinking), only the ROW itself stays fixed to one line. Neutral gray background instead of
   teal (user requirement), content centered instead of left-aligned. The grade's color is
   deliberately neutral (var(--text)) — only the trend arrow next to it stays colored. */
.school-card-stats-equal {
  display: flex;
  gap: 6px;
  margin-bottom: 12px;
}
.school-card-stat-block {
  flex: 1;
  min-width: 0;
  background: var(--stat-block-bg);
  border-radius: 10px;
  padding: 9px 5px;
  text-align: center;
}
/* The fourth box (Demand Ratio) is web/iPad ONLY (user requirement: "no room on mobile") —
   mobile stays at the previous three. display:none instead of a dedicated JS render path: the
   card isn't re-rendered on every resize (only on a filter/sort change), whereas a plain CSS
   toggle reacts instantly and always stays in sync with the current width. The other three
   boxes automatically stay equal width thanks to .school-card-stat-block { flex:1 },
   whether there are three or four of them here. */
.school-card-stat-block--demand { display: none; }
@media (min-width: 900px) {
  .school-card-stat-block--demand { display: block; }
}
.school-card-stat-block .stat-k {
  display: block;
  font-size: 0.58rem;
  font-weight: 700;
  line-height: 1.2;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--text-muted);
  margin-bottom: 2px;
  /* Single long words with no space (e.g. "Durchschnittsnote", "Sozialindex") otherwise have no
     wrap point and, with three narrow boxes side by side, run past the box edge sideways
     instead of wrapping onto a second line — break-word allows a mid-word break, but only once
     there's genuinely no other way it fits. */
  overflow-wrap: break-word;
  word-break: break-word;
  /* ALWAYS reserves the height of two lines (user feedback: values sat at inconsistent
     heights depending on whether their own label wrapped onto one or two lines) — short labels
     like "Graduates" deliberately leave some breathing room this way, but the number below it
     (2.15 / 93 / 5) lands at the same height in all three boxes, guaranteed, regardless of how
     long that particular label is. */
  min-height: 2.4em;
}
.stat-v-equal {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: 'Manrope', inherit;
  font-size: 1.08rem;
  font-weight: 600;
  color: var(--text);
  line-height: 1;
  white-space: nowrap;
  /* The actual reason for the ~3px offset (user feedback): Graduates has NO trend arrow, so
     its row is only text-tall (line-height:1). Average Grade/Sozialindex have the 1.35em-sized
     arrow as a child — their flex row becomes noticeably taller because of it, and
     align-items:center centers the number WITHIN its own (differently tall) row. Same starting
     position at the top + a different row height inevitably produces a different number center.
     min-height forces the SAME row height as the arrow (1.35em) on all three boxes, regardless
     of whether an arrow is present. */
  min-height: 1.35em;
}
/* A remaining visual correction, measured: align-items:center lines up the BOXES, but the
   numbers don't sit centered within their box (ascender-to-baseline sits above the box center,
   because room for descenders is reserved below) — 1px back brings the arrow to within
   ±0.1px of the number's center. */
.stat-v-equal .trend-arrow,
.stat-v-equal .trend-nodata-icon { transform: translateY(-1px); }
.school-card-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 18px;
  margin-bottom: 12px;
}
.school-card-stat { font-size: 0.82rem; }
.school-card-stat .stat-k { color: var(--text-muted); margin-right: 4px; }
/* Like in the table (ALIGNMENT.md §6): the value and trend indicator stay together on one
   line. The label text before it can still wrap, just not the pair. inline-flex (not flex), so
   the value still sits next to its label on the same line. */
.school-card-stat .stat-v {
  font-weight: 700;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
}
/* Location row: the achievement badge is NO LONGER a flex sibling (see .card-achievement-
   badge: position:absolute) — the row height therefore comes exclusively from the location
   text itself, always identical with or without a badge. position:relative only serves as the
   anchor for the absolutely positioned badge. */
.school-card-location-row {
  position: relative;
  margin-bottom: 12px;
}
.school-card-location {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 0.78rem;
  color: var(--text-muted);
  min-width: 0;
}
.school-card-location svg { width: 12px; height: 12px; flex-shrink: 0; }
/* Never wrap, truncate with an ellipsis instead of going to two lines (the same kind of
   safeguard as .school-card-name): a line wrap here would once again make the row height
   dependent on text length. */
.school-card-location span {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Reserves room on the right for the absolutely positioned badge, ONLY if one actually exists
   (the has-badge class, see cardHtml() in app.js) — otherwise the ~75% of cards without a badge
   would unnecessarily get less room for the location text. Only applies from 900px up, see the
   mobile override below — there the location shares the row with the type badge instead of the
   achievement badge. */
@media (min-width: 900px) {
  .school-card-location-row.has-badge .school-card-location {
    padding-right: 76px;
  }
}
/* Mobile (user requirement: "we have vertical room"): the type badge (Gymnasium/
   Stadtteilschule) moves from the name row into this row, next to the location — the markup
   has it TWICE for this (see cardHtml()), only the mobile copy is shown here. The achievement
   badge (Top X%, if present) gets its own row below, instead of floating absolutely above the
   location like it does from 900px up — for that it leaves its special positioning there and
   becomes a normal, flowing element. If there's no room left for a badge, the distance row or
   the stat boxes follow directly (unchanged, no empty placeholder).
   The line break does NOT come from flex-basis:100% on the badge itself (first attempt, user
   feedback: that made the pill take up the whole row width as its background area instead of
   just wrapping its text) — flex-basis determines the element's own MAIN-AXIS SIZE, so 100%
   here means "as wide as the row." Instead, an invisible .school-card-row-break placeholder
   (its own empty <span>, only when a badge exists, see cardHtml()) forces the break, the badge
   itself stays flex:0 0 auto (content width) and moves via margin-left:auto — like Gymnasium/
   Stadtteilschule next to it — to the right edge of its own (new) row. */
@media (max-width: 899px) {
  .school-card-name-row > .badge { display: none; }
  .school-card-location-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 10px;
  }
  .school-card-location-row .school-card-location { flex: 1 1 auto; min-width: 0; }
  .school-card-location-row > .badge { flex: 0 0 auto; }
  .school-card-row-break { flex-basis: 100%; width: 0; height: 0; }
  .card-achievement-badge {
    position: static;
    top: auto;
    right: auto;
    transform: none;
    flex: 0 0 auto;
    margin-left: auto;
  }
  /* Distance + badge share ONE row on mobile when both are present (user requirement:
     "when distance appears, it should come to the Top X% row") — the standalone
     .school-card-stats row further down is hidden for this case via --desktop-only; instead a
     second copy (--badge-row, see cardHtml()) appears here right before the badge: distance on
     the left, the badge stays right-aligned thanks to margin-left:auto. margin-bottom:0, because
     here the row is just ONE flex child among several, not its own block row like in the normal
     case. */
  .school-card-stats--desktop-only { display: none; }
  .school-card-stats--badge-row { margin-bottom: 0; flex: 0 1 auto; }
}
@media (min-width: 900px) {
  .school-card-location-row > .badge { display: none; }
  /* The mobile copy only exists in the markup when BOTH distance and the badge are present
     (see above) — from 900px up it stays unchanged at the standalone row, this copy is never
     needed. */
  .school-card-stats--badge-row { display: none; }
}
/* All-day + Languages (user requirement): boxless, icon + short text, always visible
   independent of the distance row above it. On mobile these stay TWO separate rows (earlier
   user feedback, still valid): on one shared row, the wrapping languages part pushed on narrow
   cards right into the area of the floating compare/favorite icon group at the bottom right —
   two fixed rows are predictable and never collide with it. .school-card-meta-group is the
   shared wrapper for this (see cardHtml() in app.js): a column by default on mobile (= exactly
   the previous behavior), from 900px up (user requirement for web/iPad) the media query below
   places both side by side on ONE row. */
.school-card-meta-group {
  display: flex;
  flex-direction: column;
}
.school-card-meta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  font-size: 0.78rem;
  color: var(--text-muted);
  margin-bottom: 4px;
}
.school-card-meta-item {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
/* Web/iPad: All-day + Languages side by side on one row instead of stacked (user requirement).
   All-day stays compact (flex: 0 0 auto, never shrinks), the languages row gets the rest of
   the width and, for a long list (some schools have 7–8 languages), wraps onto multiple lines
   WITHIN its own column instead of blowing out the card's width — min-width:0 on both the row
   AND the item is necessary, otherwise the implicit flex minimum width (= content with no wrap)
   would prevent exactly the shrinking the wrap needs. */
@media (min-width: 900px) {
  .school-card-meta-group {
    flex-direction: row;
    align-items: flex-start;
    gap: 6px 20px;
  }
  .school-card-meta-group .school-card-meta-row { margin-bottom: 0; }
  .school-card-meta-row--ganztag { flex: 0 0 auto; }
  .school-card-meta-row--lang {
    flex: 1 1 auto;
    min-width: 0;
  }
  .school-card-meta-row--lang .school-card-meta-item {
    min-width: 0;
  }
  .school-card-meta-row--lang .school-card-meta-item span {
    white-space: normal;
    overflow-wrap: break-word;
  }
}
.school-card-meta-item svg { width: 12px; height: 12px; flex-shrink: 0; }

.adv-filters {
  margin-top: 16px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--bg);
}
.adv-filters summary {
  padding: 12px 16px;
  cursor: pointer;
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--text);
  list-style: none;
  display: flex;
  align-items: center;
  gap: 6px;
}
.adv-filters summary::-webkit-details-marker { display: none; }
.adv-filters summary::before {
  content: '▸';
  color: var(--accent);
  display: inline-block;
  transition: transform 0.15s ease;
}
.adv-filters[open] summary::before { transform: rotate(90deg); }
.adv-filters-body {
  /* No more --content-inset right inset and no separate divider line (user requirement): both
     were left over from the old .adv-filters variant (see above, no longer used in the markup
     of the current sidebar) and caused an unwanted line right below "Detailed Criteria" in the
     sidebar AND kept District/Neighborhood/All-day/Languages/Sozialindex/Grade from reaching
     the sidebar's right edge like the address field next to them. The actual separation from
     "Location & Distance" is already provided by .sidebar-section's border-top. */
  display: flex;
  flex-direction: column;
  /* Consistent spacing between two control blocks throughout the filter card — the same value
     is set on .radius-filter-row in step 2. */
  gap: 20px;
}
.adv-filter-group { display: flex; flex-direction: column; gap: 7px; }
.adv-filter-group > label {
  font-size: 0.76rem;
  font-weight: 700;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
/* Multi-select dropdown (District/Neighborhood/All-day model/Languages): clicking .ms-trigger
   opens a checkbox panel; each selected option additionally appears as its own, individually
   removable chip below the field — the selection stays visible this way, even after the panel
   closes again. */
.ms-dropdown { position: relative; }
.ms-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 9px 12px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--card-bg);
  color: var(--text);
  font-size: 0.9rem;
  font-family: inherit;
  cursor: pointer;
  text-align: left;
  transition: border-color 0.15s ease;
}
.ms-trigger:hover { border-color: var(--accent-light); }
.ms-trigger-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ms-chevron { width: 14px; height: 14px; flex-shrink: 0; margin-left: 8px; color: var(--text-muted); transition: transform 0.15s ease; }
.ms-dropdown.open .ms-chevron { transform: rotate(180deg); }
/* position:fixed instead of absolute: .card has overflow:hidden (for the decorative background
   blob in the rounded corners), which would clip a panel positioned relative to the trigger at
   the card edge as soon as it extends past the card's bottom edge (measured: the languages
   panel lost 89px at the bottom). Fixed positions relative to the viewport, so it bypasses
   EVERY overflow:hidden/auto of an ancestor — position/width/height come from JS
   (see setupMultiSelect: position() via getBoundingClientRect).

   WATCH OUT, gotcha: "fixed" is ONLY viewport-relative if NO ancestor has set a
   transform/filter/perspective/backdrop-filter/will-change — otherwise that ancestor becomes
   the containing block and the same coordinates land offset by its position instead. Exactly
   that happened here: .card carries transform:translateY(0) via .scroll-reveal.revealed and
   additionally translateY(-4px) via .card:hover, which made the panel open offset to the
   right/down by the card's position. That's why setupMultiSelect re-attaches the open panel to
   <body> — the ancestor chain there is guaranteed transform-free. General rule for future
   overlapping UI elements (dropdown, popup, tooltip): position:fixed + JS coordinates AND
   attach the element to <body>, otherwise the positioning breaks as soon as someone adds a
   transform above it. */
.ms-panel {
  position: fixed;
  /* Above the mobile filter sheet overlay (.browse-sidebar, z-index:400) — the same cause as
     .autocomplete-list above: the Detailed Criteria dropdowns themselves live inside this
     sheet, so their panel attached to <body> must sit above the sheet itself, otherwise it
     "opens" invisibly behind it. */
  z-index: 420;
  max-height: 240px;
  overflow-y: auto;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow-hover);
  padding: 6px;
}
.ms-panel.hidden { display: none; }
.ms-option {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 8px;
  border-radius: 8px;
  cursor: pointer;
  font-size: 0.85rem;
  color: var(--text);
}
.ms-option:hover { background: var(--row-hover); }
/* "All" stands out as a bulk action from the individual values below it. */
.ms-option-all {
  font-weight: 700;
  border-bottom: 1px solid var(--border);
  border-radius: 8px 8px 0 0;
  margin-bottom: 2px;
}
.ms-option input { accent-color: var(--accent); cursor: pointer; flex-shrink: 0; }
.ms-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
/* No empty chip row when there's no selection: otherwise it would leave an invisible 8px under
   every dropdown, which made the spacing between dropdown groups (24px) and slider groups
   (16px) drift apart. */
.ms-chips:empty { display: none; }
.ms-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 6px 4px 10px;
  border-radius: 999px;
  border: 1px solid var(--accent-light);
  background: var(--accent-bg);
  color: var(--accent-dark);
  font-size: 0.76rem;
  font-weight: 600;
}
.ms-chip button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  border: none;
  background: none;
  padding: 0;
  border-radius: 50%;
  color: var(--accent-dark);
  cursor: pointer;
  font-size: 0.9rem;
  line-height: 1;
}
.ms-chip button:hover { background: rgba(15, 23, 42, 0.12); }
/* Labels now sit ABOVE the track instead of next to it (user requirement, see ALIGNMENT.md
   §11): one grid row with two named areas up top (start/end) and the track below spanning the
   full width. Grid areas instead of reordering the HTML, because the DOM (start label, track,
   end label) stays unchanged — only the visual position changes. This gives the track the
   entire row width instead of just "row minus two fixed label boxes," which makes it
   noticeably easier to grab on touchscreens. */
.adv-range-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-areas: "start end" "track track";
  row-gap: 4px;
}
/* A two-handle range slider as ONE connected bar instead of two separate mini-tracks with a
   "–" between them: two native <input type="range"> sit exactly on top of each other
   (position: absolute in .dual-range), their own track is invisible (only the thumb is
   clickable, via pointer-events: none on the input + pointer-events: auto on the
   ::-webkit/-moz thumb) — .dual-range-track/.dual-range-fill below draw the actual look. */
.dual-range { grid-area: track; position: relative; width: 100%; min-width: 60px; height: 20px; display: flex; align-items: center; }
.dual-range-track {
  position: absolute;
  left: 0; right: 0;
  height: 4px;
  border-radius: 2px;
  background: var(--border);
}
.dual-range-fill {
  position: absolute;
  height: 4px;
  border-radius: 2px;
  /* Muted teal instead of saturated (user requirement) — the handle stays var(--accent), since
     it's the actual action surface (draggable); the fill is a pure range indicator. */
  background: var(--accent-muted);
}
.dual-range-input {
  position: absolute;
  left: 0; right: 0;
  width: 100%;
  margin: 0;
  background: transparent;
  -webkit-appearance: none;
  appearance: none;
  pointer-events: none;
}
.dual-range-input::-webkit-slider-runnable-track { background: transparent; }
.dual-range-input::-moz-range-track { background: transparent; }
/* Handle enlarged to 20px (from 16px) and with a transparent halo on hover/active (user
   requirement): touchscreens have no real hover, but :active fires there on tap/drag just like
   on desktop with the mouse button held down — the halo confirms "this is currently being
   grabbed" in both cases. --accent-bg instead of a new color, since it already exists as a
   light teal-tint token for exactly this kind of selection/hover state. :not(:disabled), so
   the disabled radius slider (see .radius-filter-row.disabled) doesn't show a halo. */
.dual-range-input:not(:disabled)::-webkit-slider-thumb {
  -webkit-appearance: none;
  pointer-events: auto;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--card-bg);
  box-shadow: 0 1px 3px rgba(15, 23, 42, 0.4);
  cursor: pointer;
  transition: box-shadow 0.15s ease;
}
.dual-range-input:disabled::-webkit-slider-thumb {
  pointer-events: auto;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--card-bg);
  box-shadow: 0 1px 3px rgba(15, 23, 42, 0.4);
  cursor: not-allowed;
}
.dual-range-input:not(:disabled)::-webkit-slider-thumb:hover,
.dual-range-input:not(:disabled)::-webkit-slider-thumb:active {
  box-shadow: 0 0 0 8px var(--accent-bg), 0 1px 3px rgba(15, 23, 42, 0.4);
}
.dual-range-input:not(:disabled)::-moz-range-thumb {
  pointer-events: auto;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--card-bg);
  box-shadow: 0 1px 3px rgba(15, 23, 42, 0.4);
  cursor: pointer;
  transition: box-shadow 0.15s ease;
}
.dual-range-input:disabled::-moz-range-thumb {
  pointer-events: auto;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--card-bg);
  box-shadow: 0 1px 3px rgba(15, 23, 42, 0.4);
  cursor: not-allowed;
}
.dual-range-input:not(:disabled)::-moz-range-thumb:hover,
.dual-range-input:not(:disabled)::-moz-range-thumb:active {
  box-shadow: 0 0 0 8px var(--accent-bg), 0 1px 3px rgba(15, 23, 42, 0.4);
}
/* Grade slider: for content reasons "better" (a lower grade) should sit on the right, like
   Sozialindex ("more favorable" = right) — a plain mirror of the whole two-handle unit (track +
   fill + both inputs together) achieves that without touching the underlying min/max/value
   values or the drag interaction. */
.dual-range-reversed { transform: scaleX(-1); }
/* Start value on the left, end value on the right, the bar between them. Both sides have the
   same fixed width, so the bars of all three sliders (radius, Sozialindex, grade) start and
   end at exactly the same spot. The row itself still spans the full content width
   (ALIGNMENT.md §1/§2): the start value sits at the left content edge, the end value at the
   right — flush with the dropdowns above. */
.adv-range-value {
  grid-area: end;
  justify-self: end;
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--accent-dark);
  white-space: nowrap;
  /* The labels now sit above the track (see .adv-range-row above), not next to it anymore, so
     their width no longer determines the track's starting point — that's now always 100% wide.
     A fixed width still makes sense though: without it the box would widen/narrow with the
     digit count while dragging ("0 km" → "10 km") and the right-aligned text would jump
     sideways because of it. Sized to the widest label — "30 km" = 39px in all three languages
     (grades 27px, Sozialindex 8px) — plus a bit of headroom. */
  width: 46px;
  text-align: right;
}
.adv-range-value-start { grid-area: start; justify-self: start; text-align: left; }
/* Layer 9: "Reset" is a secondary/utility action (spec: sort/reset/filter trigger = coral). A
   ghost button (transparent background) — text/icon are therefore var(--coral) itself, not
   --coral-text: the latter is only meant for text ON a solid coral fill and in the dark theme
   (--bg #0b1622 vs. --coral-text #0f172a) sat practically on the page color, which made
   "Reset" unreadable. */
.filter-reset-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 10px 18px;
  border-radius: 999px;
  border: 1px solid var(--coral);
  background: transparent;
  color: var(--coral);
  font-size: 0.85rem;
  font-weight: 700;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color 0.15s ease, color 0.15s ease, transform 0.15s ease;
}
.filter-reset-btn:hover { background: rgba(242, 115, 92, 0.12); transform: translateY(-1px); }
.filter-reset-btn:active { transform: scale(0.95); }
.filter-reset-btn svg { width: 14px; height: 14px; }
/* Mobile only (user requirement): an "Apply" button next to Reset, sitting in completely
   normal flow at the end of the filter list (no floating/sticky — user requirement: "just
   position it simply below the filters"). There's no sheet to close on desktop/iPad, so it's
   hidden by default here — the actual look only comes in the mobile media query further down. */
.apply-filters-btn { display: none; }

.filter-steps { display: flex; flex-direction: column; gap: 22px; margin-top: 22px; }
/* The earlier negative margin-top here pulled step 3 up, because step 2's status row
   permanently reserved an empty row via min-height. That row now hides itself when empty
   (.search-status:empty), so the normal 22px step spacing applies again. */
.filter-step { display: flex; gap: 14px; }
/* Neutral instead of teal (user requirement): the 1-2-3 step number is pure orientation
   ("where am I in the flow"), not an action. */
.filter-step-badge {
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--header-bg);
  border: 1px solid var(--border);
  color: var(--text);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 800;
  font-size: 0.85rem;
}
.filter-step-body { flex: 1 1 auto; min-width: 0; }
.filter-step-title {
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--text);
  margin: 4px 0 12px;
}
.filter-step-optional {
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--text-muted);
  margin-left: 6px;
  opacity: 0.8;
}
details.filter-step { display: block; }
details.filter-step > summary {
  display: flex;
  align-items: center;
  gap: 14px;
  cursor: pointer;
  list-style: none;
}
details.filter-step > summary::-webkit-details-marker { display: none; }
details.filter-step > summary .filter-step-title { margin: 0; }
/* Replaces the earlier ::after triangle (too small, tied to the card edge via
   margin-left:auto instead of the title text) with a real SVG right after the title text: no
   more margin-left:auto, it simply follows as the last flex child with the same 14px gap as
   badge→title, stays vertically centered to the title text via align-items:center, and can be
   sized exactly, independent of character-set rendering. */
.step3-chevron {
  width: 24px;
  height: 24px;
  margin-left: -4px;
  flex-shrink: 0;
  color: var(--accent);
  transition: transform 0.15s ease;
}
details.filter-step[open] .step3-chevron { transform: rotate(90deg); }
details.filter-step .adv-filters-body {
  margin-left: var(--content-inset);
  margin-top: 12px;
  border-top: 1px solid var(--border);
  padding-top: 14px;
}

.results-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  /* No more --content-inset padding (user requirement, same as .cards-grid/.map-view above)
     — this row is now ALWAYS the topmost one in the results area and should start flush with
     the sidebar/filter, not indented. */
}
/* A shared round icon button for Sort + (on mobile) Filter — replaces the earlier
   "Explanations" text link (user requirement: only icons in this row now). */
.toolbar-actions-right { display: flex; align-items: center; gap: 8px; }
.toolbar-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  width: 36px;
  height: 36px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--bg);
  /* var(--text) instead of --text-muted (user requirement): Language/Theme in the header
     looked noticeably bolder/darker than Sort/Filter next to them — the only difference was
     this text color, not the round border (which deliberately stays unchanged,
     --border/999px/36px). */
  color: var(--text);
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease, transform 0.15s ease;
}
.toolbar-icon-btn:hover { border-color: var(--accent-light); color: var(--accent-dark); }
.toolbar-icon-btn:active { transform: scale(0.93); }
/* Sort + Filter — mobile only, where the filter button is even visible at all (desktop always
   has the sidebar open, see @media (min-width:900px) below). Both the button and the icon grow
   here (user requirement: "on mobile ... the filter and sort icons are really quite small,
   make them as big as language and theme") — 36px/20px looked noticeably smaller next to the
   34px-tall Language/Theme pair. */
@media (max-width: 899px) {
  .toolbar-icon-btn { width: 42px; height: 42px; }
  .toolbar-icon-btn svg { width: 22px; height: 22px; stroke-width: 2.3; }
}
.sort-dropdown { position: relative; }
#sortTriggerBtn[aria-expanded="true"] { border-color: var(--accent); color: var(--accent-dark); }
/* position:fixed + attached to <body> while open, like .ms-panel/.autocomplete-list —
   otherwise an overflow:hidden ancestor would clip the panel. Its own, lightweight positioning
   (see positionSortPanel() in app.js) instead of the shared input-width helpers, because the
   anchor here is a small square button, not a full input row. */
.sort-panel {
  position: fixed;
  width: 240px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: var(--shadow-hover);
  padding: 6px;
  z-index: 300;
  /* A safety net since the list grew from 5 to 10 explicit direction options — otherwise
     there's not enough room left to the viewport edge on short/landscape screens. */
  max-height: min(360px, calc(100vh - 80px));
  overflow-y: auto;
}
.sort-option {
  padding: 9px 12px;
  border-radius: 8px;
  font-size: 0.88rem;
  color: var(--text);
  cursor: pointer;
}
.sort-option:hover { background: var(--row-hover); }
/* Marks the currently active sort — with 10 explicitly spelled-out rows (instead of the old 5
   with implied direction), a visible anchor point helps find the current state at a glance,
   without having to re-read the panel text. */
.sort-option[aria-selected="true"] { background: var(--accent-bg); color: var(--accent-dark); font-weight: 700; }
.explain-section { margin-bottom: 16px; }
.explain-section:last-child { margin-bottom: 0; }
.explain-section h4 { margin: 0 0 6px; font-size: 0.92rem; font-weight: 700; color: var(--accent-dark); }
.explain-section p { margin: 0 0 8px; font-size: 0.86rem; line-height: 1.55; color: var(--text-muted); }
/* No more --content-inset indent (user preference, same as .cards-grid above): table/cards/
   map should all have exactly the same width — the previous narrow mobile special case for
   this is dropped too, it's now the normal case at all widths. */
/* position:relative + z-index:0 opens its OWN stacking context: Leaflet's internal z-index
   values (.leaflet-top/.leaflet-bottom controls: 1000; .leaflet-pane: up to 700) are otherwise
   not scoped to their container, but compete directly at the topmost stacking level with
   fixed-positioned UI elements like the mobile filter sheet overlay (.browse-sidebar,
   z-index:400) or the compare pill — that's exactly what caused the map to render literally
   on top of the sheet, zoom buttons included, on mobile/iPad. With its own stacking context,
   Leaflet's high values stay contained within .map-view. */
.map-view { margin-top: 16px; position: relative; z-index: 0; }

/* Neutral instead of teal (user preference): the result count is data info, not an action/selection. */
.results-count-hero {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* Content indent like the filter card (ALIGNMENT.md rule 1) — the pill is content, not a
     title, so it starts at --content-inset instead of the card edge. */
  margin: 2px 0 4px var(--content-inset);
  padding: 6px 14px;
  border-radius: 999px;
  background: var(--header-bg);
  border: 1px solid var(--border);
}
.results-count-number {
  font-family: 'Manrope', inherit;
  font-size: 1rem;
  font-weight: 600;
  color: var(--text);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.results-count-label {
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--text-muted);
}

.compare-bar {
  position: fixed;
  left: 50%;
  bottom: 20px;
  transform: translateX(-50%);
  z-index: 150;
  display: flex;
  align-items: center;
  gap: 14px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 10px 10px 10px 20px;
  /* --shadow instead of --shadow-hover (user preference: "too much shadow" under Compare/Reset) —
     --shadow-hover is meant as a HOVER lift for cards (strong blur/spread); as the permanent
     state of an always-visible pill it felt too heavy. */
  box-shadow: var(--shadow);
}
.compare-bar span { font-size: 0.88rem; font-weight: 600; color: var(--text); }
.compare-bar button {
  padding: 9px 20px;
  border-radius: 999px;
  border: none;
  /* Clearly visible gradient (user preference: the first version with --primary-hover/--primary
     still looked "flat", the two tones were too close together) — now three stops from
     --primary-light to --primary-hover, plus a top highlight rim (inset highlight) for a
     noticeably more plastic/3D button instead of a flat colored surface. */
  background: linear-gradient(135deg, var(--primary-light) 0%, var(--primary) 55%, var(--primary-hover) 100%);
  color: var(--primary-contrast);
  font-size: 0.85rem;
  font-weight: 700;
  cursor: pointer;
  /* Weaker than the first version (user preference: "too much shadow" under Compare/Reset) — the
     gradient alone already provides the 3D effect, the drop shadow just needs to subtly
     underline it instead of overpowering it. */
  box-shadow: 0 3px 8px -3px rgba(3, 105, 161, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.35);
  transition: transform 0.15s ease;
}
.compare-bar button:active { transform: scale(0.95); }
/* Coral instead of neutral (user preference: "aren't resets supposed to be coral?") — per spec
   (see .filter-reset-btn above), Reset is always the secondary/utility coral action, which
   wasn't the case here yet: --text-muted/--border felt not only inconsistent with the sidebar
   reset, but was barely readable on the dark .compare-bar pill in dark theme. */
.compare-bar button.compare-reset-btn {
  background: transparent;
  color: var(--coral);
  border: 1px solid var(--coral);
  font-weight: 600;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.15s ease;
}
.compare-bar button.compare-reset-btn:hover { background: rgba(242, 115, 92, 0.12); }
/* The same reset button as in the pill, but also usable outside .compare-bar (header bar of
   the Compare page) — one look, two mounting spots. */
.compare-reset-btn {
  padding: 9px 20px;
  border-radius: 999px;
  background: transparent;
  color: var(--coral);
  border: 1px solid var(--coral);
  font-family: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.15s ease;
}
.compare-reset-btn:hover { background: rgba(242, 115, 92, 0.12); }
.compare-reset-btn:active { transform: scale(0.95); }

.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(15, 23, 42, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 300;
  /* Same edge spacing as the page's content area (§8 ALIGNMENT.md), not a separate fixed value:
     otherwise the modal's edge is 8px narrower than the table's edge behind it on the phone
     (24px fixed value vs. 16px --page-gutter), and the modal looks narrower than the content
     it's describing. Matches exactly at both breakpoints via the token. */
  padding: var(--page-gutter);
}
.modal-box {
  background: var(--card-bg);
  border-radius: 18px;
  padding: 24px;
  max-width: 780px;
  width: 100%;
  max-height: 84vh;
  overflow-y: auto;
  box-shadow: var(--shadow-hover);
}
.modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 16px;
}
.modal-head h3 { margin: 0; font-size: 1.15rem; font-weight: 700; }
.modal-close {
  border: none;
  background: var(--bg);
  color: var(--text-muted);
  width: 32px;
  height: 32px;
  border-radius: 50%;
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  transition: transform 0.15s ease;
}
.modal-close:hover { transform: scale(1.08); }
.modal-close:active { transform: scale(0.92); }
/* NO table-layout:fixed: the Academics-tab sparkline needs real content overflow so that
   .compare-scroller can keep scrolling horizontally (see the .compare-tab-panel comment) —
   fixed would cut off this overflow mechanism. Instead, each column gets an equal-size width
   hint via <colgroup> (JS, compareColgroupHtml): in the normal case (content fits) the browser
   distributes the columns evenly this way, and when content is too wide the affected column is
   still allowed to grow instead of being clipped. */
.compare-table { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
.compare-table th, .compare-table td {
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
  text-align: left;
  vertical-align: top;
}
.compare-table th:first-child, .compare-table td:first-child {
  color: var(--text-muted);
  font-weight: 600;
  white-space: nowrap;
}
.compare-table th:not(:first-child), .compare-table td:not(:first-child) { text-align: center; }
.compare-table tr:last-child th, .compare-table tr:last-child td { border-bottom: none; }
/* Colored, bold number instead of neutral body text in the cell, so grade average/social index
   stand out while comparing — but at table font size (no separate font-size anymore, per
   feedback: the previous 1.3rem size felt too dominant next to the other rows). flex-wrap:nowrap
   + flex-shrink:0 on the arrow guarantee the value and trend arrow stay together even in
   narrow columns (mobile, table-layout:fixed). */
.compare-stat-value {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-wrap: nowrap;
  font-weight: 700;
  color: var(--accent-dark);
}

/* Full-width section heading/note inside a .compare-table (Layer 5.5, Programs tab: MINT/STEM,
   Special Focus, upper secondary as three sub-sections of ONE table instead of three separate
   ones). th:first-child above otherwise dictates nowrap/muted — deliberately overridden here,
   since the cell spans the full width via colspan and should wrap normally. Selectors
   deliberately use "th" AND two classes (instead of just the own class) to reach exactly the
   same specificity as ".compare-table th:first-child" — otherwise, despite coming later in the
   stylesheet, that rule would still win because of its extra element selector "th". */
.compare-table th.compare-section-heading, .compare-table th.compare-section-note {
  white-space: normal;
  text-align: left;
}
.compare-table th.compare-section-heading {
  padding-top: 18px;
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}
.compare-section-row:first-child .compare-section-heading { padding-top: 10px; }
.compare-table th.compare-section-note {
  font-weight: 400;
  font-style: italic;
  color: var(--text-muted);
}

/* Chip list for multi-value entries in a comparison cell (Special Focus, available upper
   secondary profiles) — centered like the rest of the data columns (.compare-table
   td:not(:first-child)). */
.compare-chip-list { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }

/* The continuous baseline is an ::after INSIDE .compare-tabs, not the edge of
   .compare-frame-body. Reason: .compare-tabs needs overflow-x:auto for scrollability with many
   tabs, and per the CSS spec any "non-visible" overflow-x automatically forces overflow-y:auto
   on the same element (even if never explicitly set) — an element trying to paint a line onto
   .compare-frame-body across THIS container's edge (e.g. via margin-bottom:-1px) would
   therefore be invisibly clipped before it could ever reach the border line. Instead, keeping
   both the line (::after, z-index 0) AND the tabs (z-index 1/2) fully INSIDE .compare-tabs' own
   box means no overlap across the container edge is needed — and so no clipping risk either.
   This is the original, robust version: a later "polish" pass replaced it with a full 4-sided
   .compare-frame-body border plus a JS-positioned "eraser" underneath it, to make a rounded
   top-left corner look clean — exactly that combination (a rounded corner aligned precisely to
   the edge of a scrolling sibling element) repeatedly broke on Safari/iOS (rounding errors in
   the curve, then a shadow that ran past the JS eraser). Back to this version because it avoids
   the problem architecturally instead of patching it layer by layer: .compare-frame-body no
   longer has any rounding or border at the top (see below), so there's no curve anything would
   need to be aligned to precisely. */
.compare-tabs {
  position: relative;
  display: flex;
  gap: 3px;
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
}
.compare-tabs::-webkit-scrollbar { display: none; }
.compare-tabs::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--border);
  z-index: 0;
}
.compare-tab {
  position: relative;
  z-index: 1;
  padding: 9px 16px;
  border: 1px solid var(--border);
  border-bottom: none;
  border-radius: 10px 10px 0 0;
  background: var(--tab-inactive-bg);
  color: var(--text-muted);
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
  flex-shrink: 0;
  transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
.compare-tab:hover { color: var(--text); }
.compare-tab.active {
  z-index: 2;
  color: var(--accent-dark);
  background: var(--card-bg);
  font-weight: 600;
  /* A bit tighter than the original value (was 8px blur/-3px spread) — a pure precaution left
     over from an earlier attempt, no longer strictly necessary now that .compare-frame-body
     below has gone back to having no top rounding/border at all: with no curve next to it,
     there's nothing a slightly overhanging shadow could visually clash with. */
  box-shadow: var(--tab-lift-shadow);
}
/* NO rounding and NO border at the top (border-top: none) — deliberately the simpler, more
   robust geometry instead of the interim "polished" version with a full 4-sided border plus a
   rounded top-left corner. That combination (a corner aligned precisely to the edge of
   .compare-tabs, its own scrolling sibling element) repeatedly broke on Safari/iOS — first a
   rounding error in the curve itself, then a shadow that ran past the JS eraser built to mask
   it. With the top square/borderless, there's no curve left that anything would need to be
   aligned to precisely: the background (var(--card-bg)) simply starts right below the tab row,
   in the same color as the active tab — the baseline for that lives entirely inside
   .compare-tabs (see its ::after), no element needs to paint beyond its own container edge. */
.compare-frame-body {
  position: relative;
  border: 1px solid var(--border);
  border-top: none;
  border-radius: 0 0 12px 12px;
  background: var(--card-bg);
  padding: 16px;
}
.compare-tab-panel.hidden { display: none; }
/* Panel = host of the edge shadows, NOT itself the scroll container — otherwise the shadows
   couldn't be anchored outside the scrolling content and would scroll away with it.
   .compare-scroller (child) carries the overflow-x. Without this separation, the Academics tab
   (fixed sparkline width, see the COMPARE_MODAL_MAX_WIDTH comment) had genuine horizontal
   overflow on narrow screens but no hint of it whatsoever — values like "Number of Graduates"
   looked as if missing instead of just being outside the visible area. Used by BOTH consumers
   of this tab structure: the comparison/profile modal and the Compare page (#compare) — one
   rule, two mounting spots. */
.compare-tab-panel { position: relative; }
.compare-tab-panel .compare-scroller {
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
}
/* Same look as .table-scroll-host's edge shadows (26px fade, gradient to transparent,
   --compare-fade-start/-end control visibility per side) — just without the rounded corners:
   the panel already sits 16px inside .compare-frame-body's own corners, so its own edges are
   straight, not the card's outer rounding. */
.compare-tab-panel.is-overflowing::before,
.compare-tab-panel.is-overflowing::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: 26px;
  pointer-events: none;
  z-index: 3;
}
.compare-tab-panel.is-overflowing::before {
  left: 0;
  background: linear-gradient(to right, var(--card-bg), transparent);
  opacity: var(--compare-fade-start, 0);
}
.compare-tab-panel.is-overflowing::after {
  right: 0;
  background: linear-gradient(to left, var(--card-bg), transparent);
  opacity: var(--compare-fade-end, 1);
}

/* Reusable building blocks (sparkline, grade dot, language matrix, mini bar): built for school
   comparison, but deliberately general enough to reuse later on a school-detail page with the
   same functions (renderSparkline, renderGradeTrendBlock, renderMiniBar, renderLanguageMatrix)
   for a single school instead of multiple comparison columns. */
.grade-trend-block { display: flex; flex-direction: column; gap: 4px; align-items: center; }
.sparkline { display: block; margin: 2px 0; color: var(--text-muted); }
/* Legend for the two lines in the enlarged grade-trend chart (Layer 5.2): its own HTML element
   instead of text/symbols inside the SVG itself — the line patterns (solid/dashed) are easier
   and sharper to build as small CSS lines than via SVG text characters, and this way the legend
   also stays normal body text for screen readers. */
.grade-trend-legend {
  display: flex;
  align-items: center;
  gap: 12px;
  font-size: 0.72rem;
  color: var(--text-muted);
}
.grade-trend-legend-item { display: flex; align-items: center; gap: 5px; }
.grade-trend-legend-swatch { display: inline-block; width: 16px; height: 0; border-top: 2px solid; }
.grade-trend-legend-swatch.is-school { border-top-color: var(--accent-dark); }
.grade-trend-legend-swatch.is-hamburg { border-top-style: dashed; border-top-color: var(--accent); }
/* Grade dot as a pure CSS dot instead of a traffic-light emoji (🟢🟡🟠) — Unicode emoji renders
   differently per OS/font and stood out as a foreign element next to the otherwise flat design.
   Same size as .dist-dot (9px), color comes inline from gradeColor() — hence no color rule of
   its own here, just shape/size. align-self:center instead of relying on the parent row's
   baseline alignment (e.g. .compare-stat-value): an empty dot has no text baseline it could
   meaningfully align to. */
.grade-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; align-self: center; flex-shrink: 0; }
/* Language matrix: filled dot = language offered, outlined-only dot = not offered — replaces
   the previous 🟢/⚪ emoji for the same reason as .grade-dot above. A tick larger than the other
   9px dots (.dist-dot/.grade-dot) — deliberately only here, the four language rows
   (English/French/Latin/Spanish) are the only spot with this feedback. */
.lang-dot { display: inline-block; width: 11px; height: 11px; border-radius: 50%; vertical-align: middle; }
.lang-dot-yes { background: var(--accent); }
.lang-dot-no { background: transparent; border: 1.5px solid var(--border); }

/* --- Narrow screens -----------------------------------------------------------------------
   Right at the end of the file, because media queries do NOT carry any extra specificity: a
   later plain rule (.filter-step further below) would otherwise override an earlier-declared
   media rule — that's exactly what happened on the first attempt (title text offset by 10px).
   Edge and content indent shrink, otherwise too much of the line at 390px goes to indentation
   (ALIGNMENT.md §1/§2 — it used to be 90px per side). */
@media (max-width: 600px) {
  /* Content indent becomes 0: content here no longer aligns to the title text, but to the LEFT
     EDGE of the markers (step badge or icon chip). That's 32px extra line width per side — at
     390px that's otherwise about a sixth of the card spent purely on indentation. The marker
     indent stays 32px, since the title text still sits behind its marker. See ALIGNMENT.md §7. */
  :root { --page-gutter: 16px; --marker-inset: 32px; --content-inset: 0px; }
  .section { padding: 28px 0; }
  .section-hero { padding: 20px 0 24px; }
  /* On narrow screens, nowrap tends to force overflow rather than a clean line — here, each
     headline line and the subtitle are allowed to wrap after all ("spill over" to a second
     line) instead of running past the viewport edge. */
  .hero-headline-line, .hero-subtitle { white-space: normal; }
  /* Layer 7: the hero is now the entire content of the landing page (filter/results moved to
     the Browse page) instead of a short lead-in before the filter section — so the earlier
     ~30-40% shrink (user feedback from Layer 6: "so the filter section becomes visible sooner")
     no longer has a reason to exist. Headline/subtitle grow back to a proportional, readable
     size; the stat cards stay unchanged and compact (user preference for this rebuild: "stats
     stay as they are"). */
  .hero-copy-centered h1 { font-size: 2.3rem; margin-bottom: 10px; }
  .hero-subtitle { font-size: 1.05rem; line-height: 1.45; }
  /* A clear break AFTER the presets+CTA "cluster" (see .hero-cta-row margin-top further above,
     deliberately reduced to 14px) — the stats move noticeably further down here, rather than
     just being set apart slightly. */
  .stat-row { margin-top: 50px; }
  .stat-card { padding: 4px 8px; }
  .stat-number { font-size: 1.3rem; }
  .stat-label { font-size: 0.62rem; margin-top: 2px; }
  /* --marker-inset is not an arbitrary value, but marker width plus its gap (ALIGNMENT.md §2).
     If the indent shrinks, both markers MUST shrink along with it, otherwise the title text
     starts somewhere different than the marker in front of it. */
  .filter-step { gap: 8px; }
  .filter-step-badge { width: 24px; height: 24px; font-size: 0.78rem; }
  .section-title { gap: 6px; }
  .icon-chip { width: 26px; height: 26px; border-radius: 9px; }
  .icon-chip svg { width: 15px; height: 15px; }

  /* Steps 1 and 2 indent their body via a flex row (badge + gap), not via --content-inset — so
     the variable alone does NOT move them. That's why the row becomes a block here and the
     badge is positioned absolutely: the body then starts at the card edge (i.e. at the badge's
     left edge), and only the title indents by --marker-inset, so it still sits behind its badge
     as before. Step 3 (details) has its badge inside <summary> and doesn't need this — its
     .adv-filters-body already hangs off --content-inset. */
  .filter-step:not(details) { display: block; position: relative; }
  .filter-step:not(details) > .filter-step-badge { position: absolute; left: 0; top: 0; }
  .filter-step:not(details) > .filter-step-body > .filter-step-title { padding-left: var(--marker-inset); }
}

/* ==========================================================================
   Compare page (its own route #compare, no longer a modal)
   ========================================================================== */

/* Full-bleed: the page runs the full window width, the background is the same gradient as the
   results area — via the existing theme variable, so it's automatically correct in light and
   dark. */
.compare-page {
  min-height: 100vh;
  background: var(--section-results-grad);
  /* Enough room at the bottom so the fixed selection pill (bottom: 20px + height) doesn't cover
     the last subject — the pill floats over the page, so it doesn't take up space of its own.
     No more padding at the top: the mini hero (see HTML) brings its own vertical padding. */
  padding: 0 0 104px;
}
.compare-page-inner {
  max-width: 1200px;
  margin: 0 auto;
  padding: 24px 24px 0;
}
/* The Compare page's title/back link now live in the shared .mini-hero component (see the
   .mini-hero* rules further below) instead of dedicated .compare-page-head/-title/-back-btn
   rules. */

/* Compare page header bar: search field + counter + reset sit on ONE shared surface, so they
   read as a single control unit instead of three parts randomly aligned side by side. Same look
   as the selection pill below (card surface, border, rounded corners) — the top and bottom
   visibly belong together. */
.compare-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-bottom: 22px;
  padding: 8px;
  border: 1px solid var(--border);
  border-radius: 16px;
  background: var(--card-bg);
  box-shadow: var(--shadow);
}
.compare-toolbar .autocomplete-wrap { flex: 1 1 260px; min-width: 0; max-width: 460px; }
.compare-toolbar input[type="search"] {
  width: 100%;
  box-sizing: border-box;
  padding: 10px 14px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  font-size: 0.95rem;
  font-family: inherit;
  transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.compare-toolbar input[type="search"]:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-bg);
}
.compare-toolbar input[type="search"]:disabled { opacity: 0.55; cursor: not-allowed; }
/* Counter + reset as their own group at the right end, separated from the search field by a
   hairline — the line turns the strip into a single unit with two zones. */
.compare-toolbar-group {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-left: auto;
  padding: 0 6px 0 14px;
  border-left: 1px solid var(--border);
}
.compare-count-badge {
  padding: 6px 14px;
  border-radius: 999px;
  background: var(--accent-bg);
  color: var(--accent-dark);
  font-size: 0.82rem;
  font-weight: 700;
  white-space: nowrap;
  transition: background-color 0.15s ease, color 0.15s ease;
}
/* At a count of 0, the accent green reads as "active" — so with an empty selection the counter
   stays neutral and only takes on color once the first school is added. */
.compare-count-badge.is-empty { background: var(--bg); color: var(--text-muted); }
.compare-reset-btn:disabled { opacity: 0.45; cursor: default; }
.compare-reset-btn:disabled:hover { background: transparent; border-color: var(--coral); color: var(--coral); }
/* Once the group wraps to a second line on narrow screens, the vertical divider no longer makes
   sense — it would then sit crosswise instead of between the two zones. */
@media (max-width: 600px) {
  .compare-toolbar-group {
    margin-left: 0;
    padding: 0;
    border-left: none;
    width: 100%;
    justify-content: space-between;
  }
}
.compare-empty {
  padding: 28px 0;
  color: var(--text-muted);
  font-size: 0.95rem;
}

/* One card wraps the entire Compare page: tab bar (.compare-tabs) + subject content
   (.compare-frame-body/.compare-tab-panel) — the same structure as in the comparison modal
   (buildCompareModalBody), just with its own outer edge instead of a modal frame. Previously
   four cards stacked on top of each other, each with a single, non-clickable folder tab.
   .compare-scroller now lives exclusively in .compare-tab-panel (see there) — both consumers
   (modal, Compare page) share this one rule. */
.compare-card { margin-bottom: 26px; }
/* The global rule for results-table headers (thead th { position: sticky; top: 0 }) would
   otherwise also apply here. On the Compare page, explicitly NOTHING should stick — the whole
   block, including the label column, scrolls away together. */
.compare-card .compare-table thead th { position: static; }

/* Header row of the school columns with the remove button. Lives inside the table itself so it
   travels along with its column during horizontal scrolling. Name first, X below it (user
   preference) — this way the remove action visibly hangs off the school it removes, instead of
   floating loosely above it. */
.compare-col-head { display: flex; flex-direction: column; align-items: center; gap: 6px; }
/* Coral instead of neutral (user preference): "remove school from comparison" is the same
   category as reset/the address X (see .filter-reset-btn/.address-clear-btn) — a reversible
   "get rid of it" action, not a pure "dismiss overlay" navigation like .modal-close/
   .sidebar-close-btn, which therefore deliberately stay neutral gray. */
.compare-col-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 1px solid var(--coral);
  background: transparent;
  color: var(--coral);
  font-size: 0.95rem;
  line-height: 1;
  font-family: inherit;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease, background-color 0.15s ease;
}
.compare-col-remove:hover { background: rgba(242, 115, 92, 0.12); color: var(--coral-hover); }
.compare-col-remove:active { transform: scale(0.92); }

/* Swipe hint: only on touch devices (pointer: coarse), only on the first subject, only once —
   JS removes it on first touch. On pointer devices it isn't even rendered. */
.compare-nudge { display: none; }
@media (pointer: coarse) {
  .compare-nudge {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 10px 0 0;
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--text-muted);
  }
  .compare-nudge svg {
    width: 20px;
    height: 20px;
    animation: compareNudge 1.6s ease-in-out infinite;
  }
}
@keyframes compareNudge {
  0%, 100% { transform: translateX(0); opacity: 0.55; }
  50% { transform: translateX(-9px); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .compare-nudge svg { animation: none; }
}

@media (max-width: 640px) {
  .compare-page-inner { padding: 16px 14px 0; }
  /* 0, not just reduced: unlike .modal-overlay below, this is a normally positioned box (no
     position:fixed), so it isn't affected by the same viewport mismatch — here, every remaining
     pixel only costs unused margin, not click safety. */
  .compare-frame-body { padding: 0; }
  /* Modal only (not on the #compare page, see the selector scope .modal-box): the folder tabs
     + table content extend past .modal-box's own 24px padding via a negative margin, down to
     8px before its real edge. .modal-head (and #compareCloseBtn inside it) stays UNTOUCHED at
     the full 24px — the click fix for .modal-overlay above depended on the ENTIRE margin from
     screen edge to button (overlay padding + box padding combined); directly shrinking
     .modal-box's own padding (even just horizontally) reproduced the same click bug again
     (verified with Playwright). The negative margin sidesteps this: it only shrinks the width
     of the tabs/table, not the header area's margin. */
  .modal-box .compare-tabs,
  .modal-box .compare-frame-body {
    margin-left: -16px;
    margin-right: -16px;
  }
}

/* Mobile: the popup moves close to the screen edge instead of mirroring the content gutter
   (16px) (ALIGNMENT.md §12) — on narrow screens every pixel of width is more valuable for table
   content than a symmetric margin to the edge behind it. Affects both #compareModal
   (profile/comparison) and #explainModal, since both share .modal-overlay. DELIBERATELY 4px, not
   0: at 0, #compareCloseBtn became reproducibly unclickable under Playwright's is_mobile/
   has_touch emulation after a page scroll (reported as covered by .modal-head/.modal-overlay
   itself, even though getBoundingClientRect/elementFromPoint confirmed the position) — the same
   layout/visual-viewport class of bug as ALIGNMENT.md §6, here triggered by a borderless fixed
   box instead of a transformed ancestor chain. Bisection (Playwright, 3-8 repeats each): 0px/2px
   reproducibly broken, 4px/6px/8px consistently stable — so 4px is the smallest margin that's
   still safe, not just a convenient middle value. */
@media (max-width: 640px) {
  .modal-overlay { padding: 4px; }
}

/* iOS/Safari automatically zooms in when an input field is focused, as soon as its font size is
   below 16px — and does NOT zoom back out afterward; the page stays enlarged and shifted. Our
   text fields render at 0.95rem ≈ 15.2px, so they sat right below that threshold. On pointer
   devices the smaller look is kept, on touch devices we raise it to 16px.
   Deliberately NOT solved via user-scalable=no / maximum-scale in the viewport meta tag: that
   would suppress the same zoom, but takes away every user's ability to pinch-zoom manually — a
   real usability restriction for what is purely a rendering problem. */
@media (pointer: coarse) {
  .controls input[type="search"],
  .search-row input[type="text"],
  .compare-toolbar input[type="search"] {
    font-size: 16px;
  }
}

/* --- "Load more" row of the card view (ALIGNMENT.md §2: content row) --------------------- */
.show-more-row {
  display: flex;
  justify-content: center;
  margin: 20px var(--content-inset) 0;
}
.show-more-row.hidden { display: none; }
.show-more-btn { width: 100%; max-width: 340px; }

/* --- Mobile: toolbar, slider labels, table height -------------------------------- */
@media (max-width: 600px) {
  /* View toggle and "explain" link on ONE line: together they came to 367px in 356px of space
     and wrapped as a result. The toggle buttons each give up 8px of inner padding, that's enough. */
  .results-toolbar { flex-wrap: nowrap; gap: 8px; }
  .view-toggle button { padding: 7px 12px; }
  .explain-link { flex-shrink: 0; }

  /* On the phone the screen is the scarce resource, not the table: a bit shorter than the 70vh
     of the large view, so some of the page is still visible below the table and it's clear that
     there's more. This is only the RESTING state (table hasn't reached the top yet) — see
     #tableScrollHost/syncTableStickyHeight() further below for the "stuck" state, which
     overrides this value via an inline style. */
  .table-scroll { max-height: 62vh; }
}

/* Mobile (user preference: "the table height can be tall enough to fill the screen once the
   user scrolls the top of the table up to the top of the screen"): once the table's top edge
   reaches the screen's top edge, the table should be able to use the entire rest of the screen
   for itself, instead of staying trapped in the tight 62vh/70vh box. */
@media (max-width: 899px) {
  /* Only active in table view — otherwise, in cards/map view, invisible empty space the height
     of a screen would remain, because min-height applies regardless of the (then empty) content. */
  body[data-view="table"] .table-sticky-stage { min-height: 100vh; }
  /* .table-scroll-host already carries the edge-shadow pseudo-elements (::before/::after,
     top/bottom:1px) — they stay bound exactly to the actual table height this way, because it's
     NOT the (artificially tall) .table-sticky-stage but only the host sitting tightly around the
     table itself that becomes sticky. */
  #tableScrollHost {
    position: sticky;
    top: 0;
  }
}

/* ============================================================================================
   Layer 8: Explore/preset page restructuring — mini hero + sidebar layout
   Replaces the previous "Filter Schools" title row + stacked 3-step form above the results
   with: a dynamic mini hero (page context instead of a static copy of the home hero) + a real
   sidebar (desktop/iPad: sticky with its own scroll; mobile: a bottom sheet that slides in from
   below). All form element IDs unchanged — only the container/presentation are new, the JS
   wiring (multi-select dropdowns, sliders, autocomplete) is untouched.
   ============================================================================================ */

/* --- Mini hero: same gradient as the home hero (the only spot outside the landing page),
   noticeably more compact, shows the page name + result count instead of the home copy. --- */
.mini-hero {
  background: var(--section-hero-grad);
  padding: 18px 0 28px;
}
.mini-hero-inner {
  max-width: var(--strip-max);
  margin: 0 auto;
  padding: 0 var(--page-gutter);
}
.mini-hero-title {
  font-family: 'Manrope', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: clamp(1.4rem, 2.6vw, 1.9rem);
  font-weight: 800;
  letter-spacing: -0.01em;
  color: #ffffff;
  margin: 0 0 4px;
  text-shadow: 0 2px 12px rgba(3, 20, 35, 0.3);
}
.mini-hero-count {
  font-size: 0.95rem;
  color: rgba(255, 255, 255, 0.88);
  margin: 0;
}

/* ==========================================================================
   School Profile page (#profile/<id>) — its own route, alongside #browse/#compare.
   ========================================================================== */
.profile-page {
  min-height: 100vh;
  background: var(--section-results-grad);
  padding-bottom: 60px;
}
/* Same dark gradient as .mini-hero (always the same fixed colors in light AND dark theme, see
   --section-hero-grad), hence the same hardcoded white text/translucent-white controls. */
.profile-header {
  background: var(--section-hero-grad);
  padding: 28px 0 24px;
}
.profile-header-inner {
  max-width: var(--strip-max);
  margin: 0 auto;
  padding: 0 var(--page-gutter);
}
.profile-header-name {
  font-family: 'Manrope', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: clamp(1.6rem, 3vw, 2.2rem);
  font-weight: 800;
  letter-spacing: -0.01em;
  color: #ffffff;
  margin: 0 0 4px;
  text-shadow: 0 2px 12px rgba(3, 20, 35, 0.3);
}
.profile-header-meta {
  font-size: 0.95rem;
  color: rgba(255, 255, 255, 0.88);
  margin: 0 0 16px;
}
.profile-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
/* Simple icon + short label, matching the plain CTA look of .popup-view-school elsewhere in the
   app rather than the hover-expand card icon group (built for a compact card corner, not a page
   header). The "active" state (favorited / already in Compare) fills solid white instead of the
   translucent resting state, mirroring .card-compare-btn.active/.card-favorite-btn.active. */
.profile-action-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 8px 14px;
  border: 1px solid rgba(255, 255, 255, 0.4);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.12);
  color: #ffffff;
  font-size: 0.85rem;
  font-weight: 600;
  font-family: inherit;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.profile-action-btn svg { width: 16px; height: 16px; flex-shrink: 0; }
.profile-action-btn:hover { background: rgba(255, 255, 255, 0.22); }
.profile-action-btn.active { background: #ffffff; border-color: #ffffff; color: var(--accent-dark); }
.profile-action-btn:disabled { opacity: 0.5; cursor: not-allowed; }

.profile-page-inner {
  max-width: var(--strip-max);
  margin: 0 auto;
  padding: 24px var(--page-gutter) 0;
}

.profile-glance { margin-bottom: 28px; }
.profile-glance-title {
  font-size: 1rem;
  font-weight: 700;
  color: var(--text);
  margin: 0 0 12px;
}
/* Fixed 2x3 grid (3 columns x 2 rows), collapsing to 2 columns x 3 rows on narrow screens —
   deliberately NOT a horizontal scroll strip or a single line, per spec. Tiles reuse the exact
   same .stat-k/.stat-v-equal look as the card view's stat blocks (.school-card-stat-block). */
.profile-glance-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
}
@media (max-width: 700px) {
  .profile-glance-grid { grid-template-columns: repeat(2, 1fr); }
}
.profile-glance-tile {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 14px 10px;
  text-align: center;
  box-shadow: var(--shadow);
}
.profile-glance-tile .stat-k { display: block; margin-bottom: 6px; }
.profile-glance-tile .stat-v-equal { font-size: 1.2rem; }
/* .card-achievement-badge is position:absolute by design (it floats over the card's location row
   there) — reused here for the Rank tile's value, it needs to sit inline like every other tile
   value instead of floating off to the tile's positioned ancestor. */
.profile-glance-tile .card-achievement-badge { position: static; transform: none; }

/* Tab content: continuous flow instead of a table (see rowDefsToProfileFlowHtml/buildCompareTable
   in app.js) — a bold section heading (only for rowDefs that define one, e.g. the Programs tab's
   MINT/Focus/Oberstufe groups) or, for a plain field, its own bold label, followed by the value,
   then a divider line before the next field/subsection. */
.profile-flow { max-width: 640px; }
.profile-section-heading {
  margin: 22px 0 4px;
  font-size: 0.78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}
.profile-flow > .profile-section-heading:first-child { margin-top: 4px; }
.profile-field { padding: 12px 0 10px; }
.profile-field-label {
  display: block;
  font-size: 0.8rem;
  font-weight: 700;
  color: var(--text-muted);
  margin-bottom: 6px;
}
.profile-field-value { font-size: 0.95rem; color: var(--text); }
.profile-field-note {
  padding: 8px 0;
  font-size: 0.85rem;
  font-style: italic;
  color: var(--text-muted);
}
.profile-divider {
  border: none;
  border-top: 1px solid var(--border);
  margin: 0;
}

/* "View full profile" entry-point link, shared by Cards/Table view and the Compare page's column
   headers (see cardHtml/tableRowHtml/compareColumnHeadsHtml in app.js). The Map popup reuses the
   existing .popup-view-school button instead (now retargeted to the same #profile/<id> route). */
.view-profile-link {
  display: inline-block;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--accent-dark);
  text-decoration: none;
}
.view-profile-link:hover { text-decoration: underline; }
.school-card > .view-profile-link { display: block; margin: 2px 0 10px; }
td .view-profile-link { margin-left: 8px; white-space: nowrap; }
.compare-col-head .view-profile-link { font-size: 0.72rem; }

/* --- Two-column layout: sidebar on the left, results on the right. Both scroll normally with
   the page (no sticky/fixed, see .sidebar-panel below), so align-items no longer plays any
   visible role here — .browse-sidebar carries no border/background of its own anyway. --- */
.browse-layout {
  max-width: var(--strip-max);
  margin: 0 auto;
  padding: 24px var(--page-gutter) 60px;
  display: grid;
  grid-template-columns: 320px 1fr;
  /* Smaller gap between sidebar and results (user preference: "minimal spacing") — 32px felt
     like an unnecessary gap on the now-wider surface. */
  gap: 20px;
}
/* The selection pill (.compare-bar) is position:fixed and therefore takes up no space in the
   normal flow — without this reserve it visibly overlaps the "load more" button at the end of
   the page (bottom:20px + ~57px of its own height = ~77px, the 60px default padding isn't
   enough). body.has-compare-bar is set in updateCompareBar() for as long as the pill is
   visible — affects all widths equally (desktop/iPad/mobile), since the pill has the same fixed
   position everywhere. */
body.has-compare-bar .browse-layout { padding-bottom: 112px; }
.browse-main { min-width: 0; }

/* --- Sidebar: desktop/iPad = completely normal flow (user preference) — neither
   position:sticky nor :fixed. It scrolls along at the same speed as the results column next to
   it, so it does NOT stick to the screen edge. If its content is shorter than the results
   column, empty space simply remains below it — that's intentional. No scroll of its own (no
   max-height/overflow-y): all the content is always visible at once, only the page itself
   scrolls. --- */
.browse-sidebar { min-width: 0; }
.sidebar-backdrop { display: none; }
.sidebar-panel-header { display: none; }
.sidebar-panel {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 18px 18px 22px;
}
.sidebar-topline {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 4px;
}
.sidebar-topline .autocomplete-wrap { width: 100%; }
.sidebar-topline input[type="search"] {
  width: 100%;
  box-sizing: border-box;
  padding: 10px 14px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  font-size: 0.92rem;
}
.sidebar-topline input[type="search"]:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-bg);
}
.sidebar-section {
  padding: 16px 0;
  border-top: 1px solid var(--border);
}
/* Reset as the last row under all criteria (user preference) — the same divider as between the
   filter sections, full width instead of an inline pill, so its effect ("reset everything
   above") is unambiguous. */
.sidebar-reset-row {
  padding: 16px 0 0;
  margin-top: 16px;
  border-top: 1px solid var(--border);
}
.sidebar-reset-row .filter-reset-btn { width: 100%; justify-content: center; }
.sidebar-topline + .sidebar-section { border-top: none; padding-top: 14px; }
/* Small, quiet facet-group heading instead of the previous large numbered step titles — in a
   sidebar all filters are equal-rank and editable at the same time, no longer a sequential
   wizard, hence no 1/2/3 badges. */
.sidebar-section-title {
  font-size: 0.76rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  margin: 0 0 12px;
}
details.sidebar-section-collapsible { padding-bottom: 4px; }
details.sidebar-section-collapsible > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  cursor: pointer;
  list-style: none;
}
details.sidebar-section-collapsible > summary::-webkit-details-marker { display: none; }
details.sidebar-section-collapsible > summary .sidebar-section-title { margin: 0; }
.step3-chevron { width: 15px; height: 15px; color: var(--text-muted); transition: transform 0.2s ease; flex-shrink: 0; }
details.sidebar-section-collapsible[open] .step3-chevron { transform: rotate(90deg); }
/* Expanding/collapsing the detail criteria is disabled (user preference) — markup/state are
   kept as-is (details/summary, [open] attribute, chevron rotation), only the controls for it
   are hidden, and clicking is prevented in app.js (advFiltersEl.querySelector('summary')
   .addEventListener('click', e => e.preventDefault())). To re-enable: remove this rule pair +
   delete the two lines in app.js again, nothing more. */
#advFilters > summary { cursor: default; }
#advFilters > summary .step3-chevron { display: none; }
details.sidebar-section-collapsible .adv-filters-body {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin-top: 14px;
}

/* The filter icon button shares .toolbar-icon-btn with the sort button and is only visible
   < 900px (see @media (min-width:900px) below) — on desktop the sidebar is permanently visible
   anyway. The button border itself is neutral like the sort button next to it (no border color
   of its own, .toolbar-icon-btn is enough) — only the count badge (.filter-badge, below) is
   coral, as an active-count indicator. */
.filter-badge {
  position: absolute;
  top: -4px;
  right: -4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 999px;
  /* Coral instead of --achievement-bg (user preference) — coral is already the token for
     secondary/utility actions (reset button, address-field-clear X) and fits here too: an
     active count isn't an "achievement", it's an indicator that something is being filtered. */
  background: var(--coral);
  color: var(--coral-text);
  font-size: 0.62rem;
  font-weight: 800;
  border: 2px solid var(--bg);
}
.filter-badge.hidden { display: none; }
/* On desktop/iPad (≥900px) the sidebar is permanently visible (no bottom sheet) — a second
   trigger for it would be redundant, per user preference. */
@media (min-width: 900px) {
  .mobile-filter-trigger { display: none; }
}

/* --- < 900px: sidebar becomes a bottom sheet, .browse-layout becomes a single-column view.
   900px coincides with the point where a 280px sidebar + a reasonable results width no longer
   fit side by side (real iPad portrait at 810px stays single-column/bottom-sheet). --- */
@media (max-width: 899px) {
  .browse-layout { grid-template-columns: 1fr; padding-top: 16px; }

  /* Bottom sheet: the sidebar itself becomes the full-screen overlay host (backdrop + sheet),
     but only becomes visible/interactive once .open is set — otherwise it would stay invisible
     but still sit in the click/tab path. */
  .browse-sidebar {
    position: fixed;
    inset: 0;
    z-index: 400;
    visibility: hidden;
    pointer-events: none;
  }
  .browse-sidebar.open { visibility: visible; pointer-events: auto; }
  .sidebar-backdrop {
    display: block;
    position: absolute;
    inset: 0;
    background: rgba(4, 20, 33, 0.45);
    opacity: 0;
    transition: opacity 0.25s ease;
  }
  .browse-sidebar.open .sidebar-backdrop { opacity: 1; }
  .sidebar-panel {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    top: auto;
    max-height: 85vh;
    /* Its own scroll like on desktop (see the base rule), just with a lower cap (85vh instead
       of 100vh-76px) — the sheet never takes up the full screen, the background/backdrop must
       always stay partly visible. */
    overflow-y: auto;
    border-radius: 16px 16px 0 0;
    border-bottom: none;
    transform: translateY(100%);
    transition: transform 0.3s ease;
    padding: 4px 18px 24px;
  }
  .browse-sidebar.open .sidebar-panel { transform: translateY(0); }
  /* User preference (correction): no floating/sticky — Reset+Apply sit completely normally at
     the end of the filter list, exactly like the standalone reset button before it (base rule
     .sidebar-reset-row above, unchanged). Here only switching the row from "one full-width
     button" to "two buttons side by side". */
  .sidebar-reset-row { display: flex; align-items: center; gap: 10px; }
  .sidebar-reset-row .filter-reset-btn { width: auto; flex: 1 1 0; }
  .apply-filters-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    flex: 2 1 0;
    padding: 10px 18px;
    border-radius: 999px;
    border: none;
    background: linear-gradient(135deg, var(--primary-light) 0%, var(--primary) 55%, var(--primary-hover) 100%);
    color: var(--primary-contrast);
    font-size: 0.85rem;
    font-weight: 700;
    cursor: pointer;
    white-space: nowrap;
    box-shadow: 0 3px 8px -3px rgba(3, 105, 161, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.35);
    transition: transform 0.15s ease;
  }
  .apply-filters-btn:active { transform: scale(0.95); }
  .apply-filters-btn svg { width: 15px; height: 15px; flex-shrink: 0; }
  .sidebar-panel-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 14px 0 10px;
    position: sticky;
    top: 0;
    background: var(--card-bg);
  }
  .sidebar-panel-title { font-weight: 800; font-size: 1rem; }
  .sidebar-close-btn {
    width: 30px;
    height: 30px;
    border-radius: 999px;
    border: none;
    background: var(--header-bg);
    color: var(--text);
    font-size: 1.2rem;
    line-height: 1;
    cursor: pointer;
  }
}

