/* IsleScout design system — ported from the Stitch project
   "IsleScout Property Intelligence Platform" (Material 3 tokens, Source Serif 4 + Geist).
   Written as plain CSS so pages render without a JS runtime. */

/* Fonts are self-hosted: no external request, so no visitor IP reaches a third party and
   the page has no render-blocking dependency it does not control. Both files are variable
   (one file spans 400 to 600), latin and latin-ext only, which is every character the
   published content uses. Both are SIL OFL, see assets/fonts/OFL.txt.
   ponytail: subsets beyond latin-ext are omitted, add greek or cyrillic if content ever
   carries it. Replaced the Google Fonts @import on 7 August 2026, which had been returning
   400 (an invalid opsz,wght tuple) since launch, so neither face had ever actually loaded. */
@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url(/assets/fonts/geist-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url(/assets/fonts/geist-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Source Serif 4';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url(/assets/fonts/source-serif-4-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Source Serif 4';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url(/assets/fonts/source-serif-4-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  /* Stitch theme tokens */
  --primary: #002531;
  --primary-container: #123b4a;
  --on-primary: #ffffff;
  --on-primary-container: #7fa5b7;
  --secondary: #23657f;
  --on-secondary: #ffffff;
  --secondary-container: #a3e0fd;
  --on-secondary-container: #21647e;
  --background: #f5faff;
  --surface: #f5faff;
  --surface-bright: #f5faff;
  --surface-container-lowest: #ffffff;
  --surface-container-low: #eaf5ff;
  --surface-container: #e4eff9;
  --surface-container-high: #deeaf3;
  --surface-container-highest: #d9e4ed;
  --surface-variant: #d9e4ed;
  --on-surface: #121d24;
  --on-surface-variant: #41484b;
  --outline: #5f6569;
  --outline-variant: #7e878d;
  --error: #ba1a1a;
  --error-container: #ffdad6;
  --on-error-container: #93000a;
  --on-tertiary-fixed-variant: #3c4b35;

  /* Spacing scale */
  --base: 4px; --xs: 8px; --sm: 16px; --gutter: 20px; --md: 24px; --lg: 48px; --xl: 80px;
  --margin-desktop: 40px; --margin-mobile: 16px;

  /* Radii */
  --r: 2px; --r-lg: 4px; --r-xl: 8px; --r-full: 12px;

  --serif: 'Source Serif 4', Georgia, serif;
  --sans: 'Geist', system-ui, -apple-system, sans-serif;
  --shell: 1280px;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--background);
  color: var(--on-surface);
  font: 400 16px/1.5 var(--sans);
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

h1, h2, h3 { font-family: var(--serif); font-weight: 600; margin: 0; }
a { color: inherit; }
img { max-width: 100%; display: block; }

/* ---------------------------------------------------------------- typography */
.display-lg { font: 600 48px/1.1 var(--serif); letter-spacing: -.02em; color: var(--primary); text-wrap: balance; }
.headline-md { font: 600 32px/1.3 var(--serif); color: var(--primary); }
.headline-sm { font: 600 24px/1.4 var(--serif); color: var(--on-surface); }
.body-lg { font-size: 18px; line-height: 1.6; color: var(--on-surface-variant); }
.body-sm { font-size: 14px; line-height: 1.4; }
.label-caps { font: 600 12px/1 var(--sans); letter-spacing: .05em; text-transform: uppercase; color: var(--outline); }
.data-mono { font: 500 14px/1 var(--sans); font-variant-numeric: tabular-nums; }
.muted { color: var(--on-surface-variant); }

/* The side padding here is the only thing holding content off the edge of the screen below
   1280px, which is every phone and most laptops. It is one class deep, so any co-class carrying
   a `padding` shorthand silently reset it: `.shell section` ran edge to edge on the homepage,
   the property page and the island page, and `.shell filterbar-row` clipped the Filters pill and
   the Map/List switch against both edges of a 375px screen. Nothing that shares an element with
   .shell may set `padding`, only `padding-block` or a side longhand. */
.shell { width: 100%; max-width: var(--shell); margin: 0 auto; padding: 0 var(--margin-desktop); }

.skip-link { position: absolute; left: -999px; z-index: 100; }
.skip-link:focus { left: var(--sm); top: var(--sm); background: var(--surface-container-lowest); padding: 12px var(--sm); border-radius: var(--r-lg); }

:where(a, button, input, select, summary):focus-visible { outline: 2px solid var(--secondary); outline-offset: 2px; }

/* ---------------------------------------------------------------------- nav */
.topnav { position: sticky; top: 0; z-index: 50; background: var(--surface); border-bottom: 1px solid var(--outline-variant); }
.topnav > .shell { display: flex; align-items: center; justify-content: space-between; gap: var(--md); min-height: 64px; }
.topnav-left { display: flex; align-items: center; gap: var(--lg); }
.brand { font: 600 24px/1.4 var(--serif); color: var(--primary); text-decoration: none; letter-spacing: -.02em; }
.topnav .brand { display: inline-flex; align-items: center; gap: 10px; }
.brand-mark { flex: none; }
.topnav nav { display: flex; gap: var(--md); }
.topnav nav a { font-size: 16px; color: var(--on-surface-variant); text-decoration: none; padding-bottom: 4px; transition: color .2s; }
.topnav nav a:hover { color: var(--secondary); }
.topnav nav a[aria-current="page"] { color: var(--secondary); font-weight: 600; border-bottom: 2px solid var(--secondary); }
.topnav-search { display: flex; align-items: center; gap: var(--xs); background: var(--surface-container-low); border: 1px solid var(--outline-variant); border-radius: var(--r); padding: 6px 12px; }
.topnav-search input { border: 0; background: transparent; font: 400 14px var(--sans); color: var(--on-surface); width: 190px; }
.topnav-search:focus-within { border-color: var(--secondary); box-shadow: 0 0 0 2px var(--secondary); }
/* The phone menu holds a second copy of the nav, the search box and the account link, and is
   the only one of the two shown below 780px. Hidden here rather than in the media query so a
   browser that never reaches the query (or a page with no stylesheet at all) shows the header
   row it was designed for, with the menu's own copy still in the HTML underneath it. */
.topnav-menu { display: none; }

/* ------------------------------------------------------------------ buttons */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--xs);
  min-height: 44px; padding: 12px 24px; border: 1px solid transparent; border-radius: 999px;
  font: 500 16px var(--sans); text-decoration: none; cursor: pointer; transition: background .2s, color .2s;
}
.btn-filled { background: var(--secondary); color: var(--on-secondary); }
.btn-filled:hover { background: var(--primary); }
.btn-outline { background: transparent; border-color: var(--secondary); color: var(--secondary); }
.btn-outline:hover { background: var(--surface-container-low); }
.btn-sm { min-height: 44px; padding: 10px 16px; font-size: 14px; }

.chip {
  display: inline-flex; align-items: center; gap: 6px;
  border: 1px solid var(--outline-variant); border-radius: 999px; background: transparent;
  min-height: 44px; padding: 11px var(--sm); font: 400 14px var(--sans); color: var(--on-surface-variant);
  text-decoration: none; cursor: pointer; transition: background .2s, border-color .2s;
}
.chip:hover { background: var(--surface-container-low); border-color: var(--secondary); }
.chip-active { background: var(--secondary-container); border-color: var(--on-secondary-container); color: var(--on-secondary-container); font-weight: 500; }
.chip-clear { color: var(--secondary); font-weight: 500; }

/* What a source tags a listing with. Deliberately not .chip: a chip is a filter control with a
   44px target and a hover state, and these are read-only facts. Sentence case, because they are
   the source's statements rather than labels. */
.taglist { list-style: none; display: flex; flex-wrap: wrap; gap: var(--xs); padding: 0; margin: 0; }
.taglist li { border: 1px solid var(--outline-variant); border-radius: 999px; padding: 5px var(--sm); font: 400 14px/1.3 var(--sans); color: var(--on-surface); }
.taglist.taglist-land li { border-color: var(--secondary); }

.pill { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--outline-variant); border-radius: 999px; padding: 4px var(--sm); font: 600 12px/1 var(--sans); letter-spacing: .05em; text-transform: uppercase; color: var(--on-surface-variant); }

/* ------------------------------------------------------- one frame per island
   Two islands cannot share one frame: each has its own outline and its own
   projection, so a marker drawn against the other falls outside its bbox. The
   frames stack instead, and every place they appear already has a height rule
   for a single map, so the grid divides that height between them rather than
   doubling the panel. */
/* The island overview's filter row, directly above its map. Borrows .field from the search bar
   and nothing else: this form is four controls and a submit, not a sticky bar. */
.island-filters { display: flex; align-items: center; flex-wrap: wrap; gap: var(--xs); margin-bottom: var(--sm); }
.island-filters .field { padding: 0; background: var(--surface-container-lowest); flex: 1 1 120px; }
.island-filters .field > label { top: 6px; left: 10px; }
.island-filters .field input, .island-filters .field select { font-size: 14px; padding: 20px 10px 6px; }
.island-filters > .btn { flex: 0 0 auto; }
@media (max-width: 780px) {
  .island-filters .field { flex: 1 1 calc(50% - 4px); }
  .island-filters > .btn { flex: 1 1 100%; justify-content: center; }
}

.island-maps { display: grid; gap: var(--sm); height: 100%; }
.island-map-frame { display: flex; flex-direction: column; gap: 6px; margin: 0; min-height: 0; }
.island-map-frame figcaption { flex: none; }
/* Each frame fills its share of the grid. The height rules elsewhere size a single map, so
   without this the hero's 640px would apply to every frame instead of to the pair. */
.island-maps .map-frame { flex: 1 1 auto; min-height: 160px; height: auto; }
.split-map .island-maps { gap: 1px; grid-auto-flow: row; }
.split-map .island-map-frame figcaption { padding: 6px var(--sm) 0; }

/* --------------------------------------------------------------------- hero */
/* Two columns: the search on the left, the just-added rail on the right. The rail is what
   stops the first screen being half empty page background; the island index below is still
   its own full-width section, so one page renders every island however many there are. */
.hero { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 400px); gap: var(--lg); padding-block: var(--lg) var(--md); }
.hero-copy { display: flex; flex-direction: column; gap: var(--md); max-width: 760px; }
.hero-copy p { margin: var(--sm) 0 0; }
.hero-side { display: flex; flex-direction: column; gap: var(--sm); align-items: stretch; }
.hero-pulse { margin: 4px 0 0; color: var(--on-surface-variant); }
.latest-rail { display: flex; flex-direction: column; gap: var(--xs); }
.latest-card {
  display: grid; grid-template-columns: 112px minmax(0, 1fr); gap: var(--sm); align-items: center;
  background: var(--surface-container-lowest); border: 1px solid var(--outline-variant);
  border-radius: var(--r-lg); overflow: hidden; text-decoration: none; color: var(--on-surface);
  transition: border-color .2s;
}
.latest-card:hover, .latest-card:focus-visible { border-color: var(--secondary); }
.latest-card img { width: 112px; height: 100%; min-height: 96px; object-fit: cover; display: block; }
.latest-body { display: flex; flex-direction: column; gap: 3px; padding: var(--xs) var(--xs) var(--xs) 0; min-width: 0; }
.latest-title { font: 600 15px/1.35 var(--sans); color: var(--primary);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.latest-facts { font-size: 14px; }
.latest-added { font: 600 11px/1 var(--sans); letter-spacing: .05em; text-transform: uppercase; color: var(--secondary); }

.search-panel {
  background: var(--surface-container-lowest); border: 1px solid var(--outline-variant);
  border-radius: var(--r-xl); padding: var(--md); box-shadow: 0 4px 20px rgba(29, 42, 48, .08);
  display: flex; flex-direction: column; gap: var(--xs);
}
.field { position: relative; display: flex; align-items: center; gap: var(--xs);
  border: 1px solid var(--outline-variant); border-radius: var(--r-lg); background: var(--surface-container-lowest);
  padding: 0; transition: border-color .2s; }
.field:focus-within { border-color: var(--secondary); box-shadow: inset 0 0 0 .5px var(--secondary); }
/* The label floats over the control, so it must stay one line: wrapped onto a second line it
   sits on top of the value instead of above it, which is how "Interest rate %" landed across
   "4.5" in the finance block on a narrow aside. A label too long for its field is cut short
   rather than allowed to cover the number. */
.field > label { position: absolute; top: 8px; left: 12px; font: 600 12px/1 var(--sans); letter-spacing: .05em; text-transform: uppercase; color: var(--outline);
  max-width: calc(100% - 24px); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The padding sits on the control, not on the box drawn around it. Measured 7 August 2026: with
   it on .field, every filter read as a 46px control and behaved as an 18px one, because the hit
   area is the input, not its wrapper. That failed the 44px rule in CLAUDE.md and the 24px floor
   in WCAG 2.2 AA. The values are chosen so the outer box is the height it always was, and the
   control now fills it. Keep any new padding on the control for the same reason. */
.field input, .field select {
  width: 100%; border: 0; background: transparent; padding: 22px 12px 6px; margin: 0;
  min-height: 44px; box-sizing: border-box;
  font: 400 16px var(--sans); color: var(--on-surface); appearance: none; cursor: pointer;
}
.field input { cursor: text; }
.field input:focus, .field select:focus { outline: none; }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--xs); }
.panel-actions { display: flex; gap: var(--xs); margin-top: var(--xs); }
.panel-actions .btn { flex: 1; }
.shortcuts { display: flex; flex-wrap: wrap; gap: var(--xs); margin-top: var(--xs); }

/* ---------------------------------------------------------------------- map */
/* A column: the map above, its key below. The key used to float in a corner of the map, which
   works only if that corner happens to be empty, and which corner is empty is a fact about the
   island rather than about the design. Measured across the three published islands, the box hid
   76 of Corfu's markers top right and 1 of Kefalonia's; moved to the corner that suits Corfu it
   hid 9 of Corfu's and 215 of Kefalonia's. There is no corner that is right for all of them, so
   the key sits outside the map and hides none of any of them. */
.map-frame {
  position: relative; border: 1px solid var(--outline-variant); border-radius: var(--r-xl);
  overflow: hidden; background: #d3e7f2; display: flex; flex-direction: column;
}
/* The map itself, and the containing block for everything drawn over it: the caption, and the
   live canvas map.js builds, which is inset to this rather than to the frame so it cannot cover
   the key below. min-height:0 so a flex item is allowed to be shorter than its content. */
.map-canvas { position: relative; flex: 1 1 auto; min-height: 0; }
.map-frame svg { display: block; width: 100%; height: 100%; }
.map-land { fill: #eef3ea; stroke: #8fa39a; stroke-width: 2; stroke-linejoin: round; }
.map-grid { stroke: rgba(35, 101, 127, .12); stroke-width: 1; }
.map-marker { fill: var(--secondary); stroke: #fff; stroke-width: 3; }
/* Three price bands, one hue, light to deep. A single-hue ramp reads as a position in a range;
   a red-to-green one would read as a verdict on the property, and Terracotta is spoken for here
   (reductions and warnings). Colour is never the only cue: the pin prints the price, the marker
   description names the band, and the popup repeats it for a reader zoomed out past the labels.

   Every marker on a banded map carries one of these five, including the two that are not a
   band. Falling through to the base colour instead is what made an unpriced record read as
   the middle band on the drawn map and the top band on the live one: the same record, two
   different prices, depending on whether the script had run. */
.map-band-1 { fill: #a3e0fd; stroke: var(--primary-container); }
.map-band-2 { fill: var(--secondary); }
.map-band-3 { fill: var(--primary); }
/* Neither of these is a price. Solid grey is "no asking price"; the dashed outline is a marker
   standing for records in two bands at once, and the difference between them survives
   greyscale, because it is a difference in shape and not only in fill. */
.map-band-none { fill: var(--surface-variant); stroke: var(--outline); }
.map-band-mixed { fill: var(--surface-container-lowest); stroke: var(--primary-container); stroke-dasharray: 6 4; }
.map-pill.map-band-1 rect { fill: #a3e0fd; }
.map-pill.map-band-1 text { fill: var(--primary); }
.map-pill.map-band-2 rect { fill: var(--secondary); }
.map-pill.map-band-3 rect { fill: var(--primary); }
.map-pill.map-band-none rect { fill: var(--surface-variant); }
.map-pill.map-band-none text { fill: var(--on-surface); }
.map-pill.map-band-mixed rect { fill: var(--surface-container-lowest); stroke: var(--primary-container); stroke-dasharray: 8 5; }
.map-pill.map-band-mixed text { fill: var(--primary); }
/* After the bands, so the record this page is about keeps its own colour rather than taking a
   band's. Red here means "this one", which is a different statement from a price. */
.map-marker-active { fill: var(--error); }
/* The same three bands on the live street map, where a pin is an HTML element rather than an
   SVG shape. Band 1 is pale, so its text and border darken to keep the label legible. */
.map-live-pin.map-band-1 { background: #a3e0fd; color: var(--primary); border-color: var(--primary-container); }
.map-live-pin.map-band-2 { background: var(--secondary); }
.map-live-pin.map-band-3 { background: var(--primary); }
.map-live-pin.map-band-none { background: var(--surface-variant); color: var(--on-surface); border-color: var(--outline); }
.map-live-pin.map-band-mixed { background: var(--surface-container-lowest); color: var(--primary); border-style: dashed; border-color: var(--primary-container); }
/* The interactive street map covers the SVG only once it has painted, so a failed or slow
   load leaves the server-rendered map visible instead of an empty grey box. */
/* Two classes deep on purpose: maplibre-gl.css loads after this file and sets
   `.maplibregl-map { position: relative }` on the same element, which would collapse it to
   zero height. */
.map-frame .map-live { position: absolute; inset: 0; opacity: 0; pointer-events: none; transition: opacity .25s ease; }
.map-frame-live .map-live { opacity: 1; pointer-events: auto; }
/* The drawn map, not the symbol defs beside it, and not the key below: scoped to the canvas
   because the svg stopped being a direct child of the frame when the key moved out of it. */
.map-frame-live .map-canvas > svg { visibility: hidden; }
/* MapLibre ships its control buttons at 29px, which is under the 44px this project asks for and
   under the 24px floor in WCAG 2.2 AA once you account for the gap between them. Sized here
   rather than only on our own recentre button, because a 44px button in a stack of 29px ones
   reads as a mistake, and the zoom controls have the same fingers pointed at them.
   Scoped under .map-frame on purpose: maplibre-gl.css sets 29px on the same selector and loads
   after this file, so an unscoped rule loses the cascade at equal specificity. */
.map-frame .maplibregl-ctrl-group button {
  width: 44px; height: 44px;
  display: flex; align-items: center; justify-content: center;
}
.map-frame .maplibregl-ctrl-recentre { color: var(--primary); background: #fff; }
.map-frame .maplibregl-ctrl-recentre:hover { background: var(--surface-container); }
.map-live-pin {
  display: block; padding: 6px 12px; min-height: 32px; border-radius: var(--r-full);
  background: var(--primary); color: #fff; border: 2px solid #fff; cursor: pointer;
  font: 600 13px/1.4 var(--sans); font-variant-numeric: tabular-nums; text-decoration: none;
  box-shadow: 0 1px 4px rgba(0, 37, 49, .3);
}
.map-live-pin:hover, .map-live-pin:focus-visible { background: var(--secondary); }
/* Zoomed out past reading distance the pin is a dot: at island height hundreds of price
   labels stack into a heap, and what reads at that height is density, not price. The text
   stays in the DOM (and in the aria-label), only its box shrinks. */
.map-dots .map-live-pin {
  font-size: 0; width: 14px; height: 14px; min-height: 0; padding: 0; border-radius: 50%;
}
/* The pin for the record you are already reading: a marker, not a control. */
.map-live-pin-here, .map-live-pin-here:hover { cursor: default; background: var(--primary); }
.map-pop { display: block; text-decoration: none; color: var(--on-surface); font: 400 13px/1.5 var(--sans); }
.map-pop strong { display: block; color: var(--primary); }
/* No text-transform: this used to sentence-case the raw `location_precision` enum the
   popup printed. The payload now carries real words, which capitalize would mangle
   back into "Village-Level Location". */
.map-pop-note { display: block; color: var(--on-surface-variant); font-size: 12px; }
/* Preview strip: the first image large, the rest as thumbs beside it. Fixed heights so a
   slow image cannot resize the popup under the pointer. */
.map-pop-shots { display: grid; grid-template-columns: 2fr 1fr; gap: 3px; margin-bottom: 6px; border-radius: var(--r-md); overflow: hidden; }
.map-pop-shots img { width: 100%; height: 44px; object-fit: cover; display: block; background: var(--surface-variant); }
.map-pop-shots img:first-child { grid-row: span 2; height: 91px; }
.map-pop-shots img:only-child { grid-column: span 2; }
.map-pop-credit { text-transform: none; }
/* Several records on one position: the popup lists them instead of stacking pins. Capped
   height so a locality with thirty records does not become a full-screen popup. */
.map-pop-group { max-height: 220px; overflow-y: auto; }
.map-pop-group a { display: flex; gap: 8px; justify-content: space-between; padding: 6px 0; border-top: 1px solid var(--outline-variant); color: var(--secondary); text-decoration: none; }
.map-pop-group a:hover { text-decoration: underline; }
.map-pop-group a span { color: var(--on-surface-variant); font-variant-numeric: tabular-nums; white-space: nowrap; }
.maplibregl-popup-content { border-radius: var(--r-xl); padding: 10px 14px; }

.map-caption {
  position: absolute; left: var(--sm); bottom: var(--sm);
  background: rgba(255, 255, 255, .93); border: 1px solid var(--outline-variant); border-radius: var(--r-lg);
  padding: 8px 12px; font: 600 12px/1.4 var(--sans); letter-spacing: .05em; text-transform: uppercase; color: var(--on-surface-variant);
  /* Leaves the bottom-right corner clear for the map attribution on a narrow screen. */
  max-width: calc(100% - 150px);
}
/* A caption that is only its disclosure icon should be the size of the icon, not of the 44px
   target: sized to the target, a 12px caption bar became a 60px box sitting on the map. The
   box is the glyph and the target is a transparent 44px square centred on it, so the hit area
   is unchanged and overhangs the bar. 2px of padding plus the 1px border puts the box at 24px,
   the same square as MapLibre's own attribution button in the opposite corner. */
.map-caption:has(> .disclose) { padding: 2px; line-height: 0; }
.map-caption > .disclose { position: relative; width: 18px; height: 18px; }
.map-caption > .disclose::before { content: ""; position: absolute; top: 50%; left: 50%;
  width: 44px; height: 44px; transform: translate(-50%, -50%); }
/* The localities on the map, as ordinary links. This is the reachable path to a record when
   the map is only a picture: rows are full width, so each target clears the 44px minimum that
   an SVG marker never could. */
.map-places { padding: var(--sm) var(--md) var(--md); background: var(--surface-bright); border-top: 1px solid var(--outline-variant); }
.map-places ul { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
.map-places a { display: flex; align-items: center; gap: 8px; min-height: 44px; padding: 6px 14px;
  border: 1px solid var(--outline-variant); border-radius: 999px; background: var(--surface-container-lowest);
  text-decoration: none; color: var(--on-surface); font: 400 14px var(--sans); }
.map-places a:hover { border-color: var(--secondary); color: var(--secondary); }
.map-places a span { color: var(--on-surface-variant); font-size: 13px; }
/* Under the map, full width, entries in a wrapping row. Nothing floats and nothing is hidden. */
.map-legend { flex: none; background: var(--surface-bright); border-top: 1px solid var(--outline-variant); padding: var(--xs) var(--sm); font-size: 12px; color: var(--on-surface-variant); }
/* The price-band key. Each swatch carries its own boundaries as text beside it, so the key is
   readable in greyscale and the colours are a shortcut to it rather than the only way in. */
/* A card's rate against its own village, under the rate rather than beside it: the two are one
   answer and a second column would read as a second measurement. */
.rate-vs { display: block; color: var(--on-surface-variant); font-size: 12px; font-weight: 400; font-variant-numeric: normal; }
.map-keys { display: flex; flex-wrap: wrap; gap: 2px var(--sm); margin-top: 4px; }
.map-key { display: flex; align-items: center; gap: 6px; font-variant-numeric: tabular-nums; }
.map-key::before { content: ""; width: 12px; height: 12px; border-radius: 50%; border: 1px solid var(--outline-variant); flex: none; }
.map-key-1::before { background: #a3e0fd; }
.map-key-2::before { background: var(--secondary); }
.map-key-3::before { background: var(--primary); }
.map-key-none::before { background: var(--surface-variant); border-color: var(--outline); }
.map-key-mixed::before { background: var(--surface-container-lowest); border: 1px dashed var(--primary-container); }
.map-legend small { display: block; margin-top: 6px; font-size: 11px; line-height: 1.4; }
/* A marker the island page's controls have filtered out. Both maps: an SVG circle and a live
   pin carry the same class, because they carry the same data-slug the filter finds them by. */
.map-off { display: none; }
.map-shown { margin-top: var(--xs); }

/* ------------------------------------------------------------ island chooser
   One card per island however many there are: the hero used to hold one map frame per
   island, which stops working past two or three. The deep map lives on the island page. */
/* 270px, not 340px: four islands must fit four-across inside the 1280px shell, because three
   cards and an orphan on its own row reads as a layout mistake rather than a grid. */
.island-index { display: grid; gap: var(--sm); grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); align-content: start; }
/* Stacked, outline above the figures, because four-across leaves each card ~280px: the old
   160px-outline-beside-text split wrapped every figure one word to a line at that width. The
   fixed outline height is also what keeps the four names on one baseline. */
.island-card {
  display: flex; flex-direction: column; gap: var(--sm);
  padding: var(--md); background: var(--surface-container-lowest);
  border: 1px solid var(--outline-variant); border-radius: var(--r-xl);
  text-decoration: none; color: var(--on-surface);
}
.island-card:hover, .island-card:focus-visible { border-color: var(--secondary); }
.island-card:hover .island-card-cta { text-decoration: underline; }
.island-card-outline { width: 100%; height: 130px; flex: none; }
.island-card-outline path { fill: var(--surface-container); stroke: var(--secondary); stroke-width: 3; vector-effect: non-scaling-stroke; }
.island-card-body { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.island-card-name { font: 600 24px/1.4 var(--serif); color: var(--primary); }
.island-card-where { color: var(--on-surface-variant); }
.island-card-median { font-size: 15px; }
.island-card-cta { margin-top: var(--xs); color: var(--secondary); font: 600 14px var(--sans); display: inline-flex; align-items: center; gap: 6px; }

/* -------------------------------------------------------------------- cards */
.section { padding-block: var(--xl); }
.section-head { display: flex; justify-content: space-between; align-items: flex-end; gap: var(--md); margin-bottom: var(--md); }
.section-head p { margin: var(--base) 0 0; color: var(--on-surface-variant); }

.bento { display: grid; grid-template-columns: 2fr 1fr; grid-template-rows: 1fr 1fr; gap: var(--sm); }
.bento > :first-child { grid-row: span 2; }

.tile {
  position: relative; display: block; min-height: 200px; border: 1px solid var(--outline-variant);
  border-radius: var(--r-xl); overflow: hidden; text-decoration: none; background: var(--surface-container);
}
.bento > :first-child.tile { min-height: 420px; }
.tile img { width: 100%; height: 100%; object-fit: cover; transition: transform .7s ease; position: absolute; inset: 0; }
.tile:hover img { transform: scale(1.03); }
.tile-scrim { position: absolute; inset: 0; background: linear-gradient(to top, rgba(0, 37, 49, .85) 0%, rgba(0, 37, 49, .15) 55%, transparent 100%); }
.tile-body { position: absolute; left: 0; right: 0; bottom: 0; padding: var(--sm); color: #fff; }
.tile-body h3 { color: #fff; font-size: 22px; }
.tile-body .label-caps { color: var(--secondary-container); }
.tile-body p { margin: 4px 0 0; font-size: 14px; color: rgba(255, 255, 255, .88); }
.tile-tag { position: absolute; top: var(--sm); left: var(--sm); background: rgba(255, 255, 255, .93); color: var(--primary); border-radius: var(--r-lg); padding: 5px 10px; font: 600 12px/1 var(--sans); letter-spacing: .05em; text-transform: uppercase; }

/* Result cards (split view) */
.result-card {
  display: grid; grid-template-columns: 200px 1fr;
  background: var(--surface-container-lowest); border: 1px solid var(--outline-variant);
  border-radius: var(--r-xl); overflow: hidden; transition: border-color .2s, box-shadow .2s;
}
.result-card:hover { border-color: var(--secondary); box-shadow: 0 2px 12px rgba(29, 42, 48, .07); }
.result-media { position: relative; background: var(--surface-container); min-height: 190px; }
.result-media img { width: 100%; height: 100%; object-fit: cover; position: absolute; inset: 0; }
.result-body { padding: var(--sm); display: flex; flex-direction: column; gap: var(--xs); }
/* Wraps rather than clips. The price carries its position on the village range beside it, so on
   a phone the two together are wider than the card and the locality was cut mid-word by the
   card's own `overflow: hidden`: "KATAVOLO". There is room for both on one line at every width
   that has it, and the locality takes a line of its own on the widths that do not. */
.result-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--sm); flex-wrap: wrap; }
.result-price { font: 600 24px/1.2 var(--serif); color: var(--primary); font-variant-numeric: tabular-nums; }
.result-title { margin: 0; font: 500 16px var(--sans); }
.result-title a { color: var(--on-surface); text-decoration: none; }
.result-title a:hover { color: var(--secondary); text-decoration: underline; }
.result-specs { display: flex; flex-wrap: wrap; gap: var(--sm); font-size: 14px; color: var(--on-surface-variant); font-variant-numeric: tabular-nums; }
.result-place { margin: 0; font-size: 13px; color: var(--on-surface-variant); font-variant-numeric: tabular-nums; }
.result-metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--xs) var(--sm); background: var(--surface-container-low); border-radius: var(--r-lg); padding: 10px 12px; margin: 0; }
.result-metrics > div { display: flex; justify-content: space-between; gap: var(--xs); font-size: 13px; }
.result-metrics dt { color: var(--on-surface-variant); }
.result-metrics dd { margin: 0; font-weight: 500; font-variant-numeric: tabular-nums; }
.result-actions { display: flex; align-items: center; justify-content: space-between; gap: var(--xs); flex-wrap: wrap; }
.compare-pick { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; cursor: pointer;
  color: var(--secondary); font-size: 14px; }
.compare-pick input { width: 18px; height: 18px; accent-color: var(--secondary); }
.result-price .delta { margin-left: 8px; vertical-align: 3px; }
.delta { display: inline-flex; align-items: center; gap: 4px; font: 500 13px var(--sans); font-variant-numeric: tabular-nums; color: var(--on-surface-variant); background: var(--surface-container-high); border-radius: 999px; padding: 3px 10px; white-space: nowrap; }
.delta-below { color: var(--on-tertiary-fixed-variant); background: #eaf1e6; }
/* A reduction in the price timeline. Terracotta is what the design system reserves for a
   reduction; the arrow and the amount beside it carry the same fact, so colour is never
   what tells a reader which way the price went. */
.delta-down { color: #8c4227; background: #f7ece7; }
.datalist dd .delta { margin-right: 8px; vertical-align: middle; }
.datalist dd .delta .ico { width: 14px; height: 14px; }
.delta-scale { position: relative; flex: none; width: 44px; height: 8px; border-radius: 999px; background: var(--outline-variant); }
.delta-scale b, .delta-scale i { position: absolute; transform: translateX(-50%); border-radius: 999px; }
.delta-scale b { top: -2px; bottom: -2px; width: 2px; background: var(--outline); }
.delta-scale i { top: 0; bottom: 0; width: 8px; background: var(--primary); }

/* ------------------------------------------------------------- split layout */
.split { display: grid; grid-template-columns: minmax(0, 700px) 1fr; align-items: start; }
.split-results { min-width: 0; border-right: 1px solid var(--outline-variant); background: var(--surface-bright); }
/* One row carrying the heading, the filters in force, the order and the basis icon. It used to
   be six stacked bands and the first property began 570px down the page, which on a phone is a
   whole screen with nothing for sale on it. Each group wraps as a unit, so a long heading takes
   a second line instead of breaking the row apart. */
.results-head { display: flex; align-items: center; gap: var(--sm) var(--base); flex-wrap: wrap; padding: 10px var(--md); border-bottom: 1px solid var(--outline-variant); background: var(--surface-container-lowest); }
/* The heading takes the first line and the controls the second, always. Sharing one line only
   works while the heading is short, and it is a sentence naming every island in the set, so at
   five names it grew and pushed the order onto a third line of its own. */
.results-head .head-row { flex: 1 1 100%; min-width: 0; gap: var(--xs); flex-wrap: nowrap; }
/* The page's h1 still, set at the size a label deserves rather than the size a headline does:
   the largest object on a results page is the results. */
.results-h1 { font: 600 17px/1.35 var(--serif); letter-spacing: -.01em; color: var(--primary); margin: 0; }
.results-head .active-filters { flex: 1 1 auto; padding: 0; }
.results-head .sortbar { margin-left: auto; }
.results-list { display: flex; flex-direction: column; gap: var(--sm); padding: var(--md); }
.results-showing { margin: 0; padding: var(--sm) var(--md) 0; text-align: center; }
/* At the foot of the page, because it submits what a reader ticks on the way down it and the
   ticks can come from more than one list. This is the state a browser without :has() gets: an
   ordinary bar in an ordinary place, at the end of the document. */
.compare-bar { display: flex; align-items: center; justify-content: space-between; gap: var(--sm);
  margin: 0 var(--md); padding: 10px var(--sm); border: 1px solid var(--outline-variant);
  border-radius: var(--r-xl); background: var(--surface-container-lowest); }
.compare-bar p { margin: 0; color: var(--on-surface-variant); }
/* The count is drawn by the browser from the boxes themselves, so it cannot disagree with what
   is ticked and it needs no script. Decorative: it is announced to a screen reader by each
   checkbox's own state, and the bar carries a written sentence for the same reason. */
body { counter-reset: picked; }
.compare-pick:has(input:checked) { counter-increment: picked; }
/* Four is what a comparison holds, so the tally names the limit rather than waiting for the
   comparison page to break the news after eight boxes were ticked. Above four it prints the
   limit, because "8 of 4 selected" is not a sentence and four is what the form will actually
   compare; the cards past the limit say so themselves, see .pick-over below. A browser that
   does not know @counter-style prints the plain number, which is the old wording back. */
@counter-style picked-capped { system: fixed 0; symbols: "0" "1" "2" "3" "4"; fallback: picked-over; }
@counter-style picked-over { system: cyclic; symbols: "4"; }
.compare-count::before { content: counter(picked, picked-capped) " of 4 selected";
  font-weight: 600; color: var(--on-surface); }

/* With :has(), the bar is a small panel that appears at the foot of the window the moment a
   first box is ticked, and follows the reader down the list. Ticking card two and then finding
   nothing to press until card twenty-four is the thing this replaces. */
/* Only visible on a card the comparison will leave out, which the rule below picks. */
.pick-over { display: none; flex-basis: 100%; color: var(--scale-high); font-size: 13px; }

@supports selector(:has(*)) {
  .compare-bar { display: none; }
  /* The fifth ticked card and every one after it. The form sends them all and the server keeps
     the first four in the order they stand in, so this chain names exactly the ones dropped.
     Not disabled: a reader who has ticked five wants to swap one, and a checkbox that cannot be
     unticked is the wrong four with no way out of them. */
  .result-card:has(input:checked) ~ .result-card:has(input:checked)
    ~ .result-card:has(input:checked) ~ .result-card:has(input:checked)
    ~ .result-card:has(input:checked) .pick-over { display: block; }
  /* Outside a results list there is no sibling run to count, so the property page's own box
     never claims to be the fifth. /compare states it on arrival instead. */
  .detail-head .pick-over { display: none; }
  body:has(.compare-pick input:checked) .compare-bar {
    display: inline-flex; position: fixed; left: var(--md); bottom: var(--md); z-index: 45;
    width: auto; max-width: calc(100% - var(--md) * 2); margin: 0;
    padding: 10px 10px 10px var(--base); gap: var(--base);
    box-shadow: 0 10px 30px rgb(0 37 49 / .18); }
  /* Clear of the cookie strip while there is one to clear. */
  body:has(.cookie-bar):has(.compare-pick input:checked) .compare-bar {
    bottom: calc(var(--md) + 48px); }
  /* Tried and dropped: hiding a "select one more" line once two are ticked. It needs one
     :has() inside another, which is not a selector, and the ways around it are a counter style
     that prints a sentence. The count says "1 selected" on its own, and /compare answers a
     single pick with a page that says two is the fewest and offers the way back. */
}
.pager { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: var(--xs);
  padding: 0 var(--md) var(--md); }
.pager-pages { display: flex; align-items: center; gap: 2px; }
.pager-page, .pager-step, .pager-gap { display: inline-flex; align-items: center; justify-content: center; gap: 4px;
  min-width: 44px; min-height: 44px; padding: 0 12px; border-radius: var(--r-lg);
  font: 500 14px var(--sans); font-variant-numeric: tabular-nums; color: var(--on-surface); text-decoration: none; }
.pager-gap { color: var(--on-surface-variant); min-width: 24px; padding: 0; }
.pager-page:hover, .pager-step:hover { background: var(--surface-container-high); }
.pager-on { background: var(--primary); color: var(--surface-container-lowest); }
.pager-on:hover { background: var(--primary); }
/* One map, one column, whatever the set spans. The frame takes the height the two stacked
   half-frames used to share; the switch above it takes its own row. `.map-places` leaves the
   flow because it is the fallback list for a map that is only a picture, and in this column it
   sits under a full-height map rather than competing with it for the same height.
   131px is the header plus the one-row filter bar: it is where the bar's underside sits, so the
   column meets it instead of leaving a strip of the list showing through. */
.split-map { position: sticky; top: 131px; height: calc(100vh - 131px); display: flex; flex-direction: column; }
.split-map .island-maps { flex: 1 1 auto; min-height: 0; }
.split-map .map-places { position: absolute; top: 100%; left: 0; right: 0; }
.split-map .map-frame { height: 100%; border: 0; border-radius: 0; }
/* With a switch above it naming the island, the frame's own caption says it a second time. */
.map-switch + .island-maps figcaption { display: none; }
.map-switch { display: flex; flex-wrap: wrap; align-items: center; gap: var(--xs);
  padding: var(--xs) var(--sm); background: var(--surface-container-lowest);
  border-bottom: 1px solid var(--outline-variant); }
.map-switch a, .map-switch .switch-on { display: inline-flex; align-items: center; gap: 8px;
  min-height: 44px; padding: 6px 14px; border: 1px solid var(--outline-variant);
  border-radius: 999px; background: var(--surface-container-lowest); color: var(--on-surface);
  text-decoration: none; font: 400 14px var(--sans); }
.map-switch a:hover { border-color: var(--secondary); color: var(--secondary); }
.map-switch .switch-on { background: var(--secondary); color: var(--on-secondary); border-color: var(--secondary); }

.filterbar { border-bottom: 1px solid var(--outline-variant); background: var(--surface-container-lowest); position: sticky; top: 64px; z-index: 40; }
.filterbar-row { display: flex; align-items: center; gap: var(--sm); }
.filters { flex: 1; min-width: 0; }
/* The panel ships closed in the markup, because on a phone an open one is 237px of controls
   standing between the reader and the first result. Above 780px there is room for it and no
   summary to open it with, so it is held open here, in the same rule that hides the summary:
   whichever of the two ever fails, the other has to fail with it or the filters become
   unreachable. Two ways of saying it because browsers disagree about how a closed panel hides
   its contents; a browser that honours neither shows the summary's own pill, which still works. */
.filters > summary { display: none; }
@media (min-width: 781px) {
  .filters:not([open]) > form { display: flex; }
  .filters:not([open])::details-content { content-visibility: visible; display: contents; }
}
.filterbar form { display: flex; align-items: center; gap: var(--xs); flex-wrap: wrap; padding: 10px 0; }
/* Island and price are chips now, not selects: one tap, no Apply, and an address of their own.
   The island row takes a line to itself because its chips carry counts and read as a sentence
   about the set; price shares the line with the type control and the two buttons. */
.filter-chips { display: flex; align-items: center; flex-wrap: wrap; gap: var(--xs); min-width: 0; }
.filter-chips[aria-label="Island"] { flex: 1 1 100%; }
.filter-chips .label-caps { color: var(--on-surface-variant); margin-right: 4px; }
.filter-chips .chip { min-height: 36px; padding: 7px 14px; }
.filter-chips .data-mono { margin-left: 2px; color: inherit; font-weight: 600; }
.filterbar .field { padding: 0; background: var(--surface-container-low); flex: 0 1 170px; }
.filterbar .field > label { top: 6px; left: 10px; }
/* Same rule as .field above: the padding belongs to the control so the control is the target. */
.filterbar .field input, .filterbar .field select { font-size: 14px; padding: 20px 10px 6px; }
.active-filters { display: flex; flex-wrap: wrap; gap: var(--xs); padding: 0 var(--md) var(--sm); }

/* Island, type and the price range on the bar; everything else behind one disclosure, inside
   the same form. A closed <details> still submits the controls inside it, so this is a layout
   change and nothing else. Open, it takes the whole row rather than a 170px column of its own. */
.more-filters { order: 1; }
.filterbar form > .btn { order: 2; }
.more-filters[open] { flex: 0 0 100%; }
.more-filters > summary { display: inline-flex; align-items: center; gap: 6px; min-height: 44px;
  padding: 10px 18px; border: 1px solid var(--outline-variant); border-radius: 999px;
  font: 500 14px var(--sans); color: var(--on-surface); background: var(--surface-container-lowest);
  cursor: pointer; list-style: none; }
.more-filters > summary::-webkit-details-marker { display: none; }
.more-filters[open] > summary { background: var(--secondary); color: var(--on-secondary); border-color: var(--secondary); }
.more-fields { display: flex; flex-wrap: wrap; align-items: center; gap: var(--xs); padding: 10px 0 2px; }

/* The order sits beside the count it applies to, as links: one click, no Apply, and every
   order keeps an address of its own. The one in force fills like a selected chip rather than
   like a button: filled near-black, it was heavier than Apply, so the darkest control on the
   page was the one that reorders a list and not the one that changes it. */
.sortbar { display: flex; align-items: center; flex-wrap: wrap; gap: 2px; }
.sortbar a, .sortbar .sort-on { display: inline-flex; align-items: center; min-height: 44px;
  padding: 6px 12px; border-radius: 999px; font: 500 13px var(--sans); text-decoration: none;
  color: var(--on-surface-variant); }
.sortbar a:hover { background: var(--surface-container-high); color: var(--secondary); }
.sortbar .sort-on { background: var(--secondary-container); color: var(--on-secondary-container); font-weight: 600; }

/* --------------------------------------------------------------- data slabs */
.slab { background: var(--surface-container-lowest); border: 1px solid var(--outline-variant); border-radius: var(--r-xl); padding: var(--md); }
.slab + .slab { margin-top: var(--sm); }
.slab > h2, .slab > h3 { margin-bottom: var(--sm); }
/* The fact table said once as a sentence, directly above the fact table. `p` keeps the browser's
   default margins site-wide, which inside a padded slab would double the gap at the top. */
.record-sentence { margin: 0 0 var(--md); font-size: 17px; line-height: 1.6; }
.datalist { margin: 0; display: grid; grid-template-columns: 1fr 1fr; gap: var(--sm) var(--md); }
.datalist > div { display: flex; justify-content: space-between; gap: var(--sm); border-bottom: 1px solid var(--surface-container-highest); padding-bottom: var(--xs); }
.datalist dt { color: var(--on-surface-variant); font-size: 14px; }
.datalist dd { margin: 0; font-weight: 500; font-variant-numeric: tabular-nums; text-align: right; }

/* The other agencies selling one property. One column, because an agency name and a price do
   not fit the two-column grid on a narrow sidebar, and the anchor fills its row so the tap
   target is the 44px the design system asks for rather than the 18px of the text itself. */
.source-list { grid-template-columns: 1fr; }
.source-list > div { align-items: center; min-height: 44px; }
.source-list dt { flex: 1; }
.source-list dt a { display: flex; align-items: center; min-height: 44px; }

/* The one indicative figure on the finance block, and the GET form that recalculates it. */
.finance-figure { margin: 0; }
.finance-figure .data-mono { font: 600 28px/1.2 var(--sans); font-variant-numeric: tabular-nums; color: var(--primary); }
.finance-form { display: flex; flex-wrap: wrap; align-items: center; gap: var(--xs); margin-top: var(--md); }
/* Narrower basis than the main column's controls: this block lives in the 300px aside now, so a
   140px basis put one field per row and made the shortest form on the site the tallest thing in
   the column. At 96px the three sit two-up and wrap to one only when the aside is at its
   narrowest. The control keeps its own 44px min-height from .field, so nothing shrinks below the
   tap target. */
.finance-form .field { flex: 1 1 96px; }
.finance-form .btn { flex: 1 1 100%; }
.finance-panel .finance-lede { margin: 0 0 var(--sm); }
.finance-panel .finance-figure .data-mono { font-size: 24px; }

/* Wide fact tables scroll inside their own box so the page body never scrolls sideways. */
.table-scroll { overflow-x: auto; margin-top: var(--sm); container-type: inline-size; }
.datatable { border-collapse: collapse; width: 100%; font-size: 14px; }
/* A caption inside a table is as wide as the table, so on a phone the sentence stating what the
   figures are and where they came from ran off the side with the columns and read as a line cut
   mid-word. It is prose, not a column: 100cqi is the width of the scroll container rather than of
   the table, and `sticky` keeps it at the left edge while the columns move under it. The same
   pair the row headings in this table already use. A browser without container query units drops
   the width and gets what it had before. */
.datatable caption { text-align: left; padding-bottom: var(--xs); position: sticky; left: 0;
  width: 100cqi; white-space: normal; }
.datatable th, .datatable td { text-align: right; padding: 10px var(--xs); border-bottom: 1px solid var(--surface-container-highest); white-space: nowrap; }
.datatable thead th { color: var(--on-surface-variant); font-weight: 500; border-bottom-width: 2px; }
/* Sticky because the locality comparison is ten columns wide and scrolls sideways on a phone:
   the name is what every other cell in the row is about, so it stays put while the rest moves.
   Without it a reader two columns in is holding numbers with no village attached. */
.datatable th[scope="row"] { text-align: left; font-weight: 500; position: sticky; left: 0; background: var(--surface-container-lowest); }
.datatable thead th:first-child, .datatable caption { text-align: left; }
.datatable td.data-mono { font-variant-numeric: tabular-nums; }
.datatable tbody tr:last-child th, .datatable tbody tr:last-child td { border-bottom: 0; }
/* The property page's three fact tables: a label with its basis on the left, one figure on the
   right, and no third column. They were 604px of table in a 293px column on a 375px screen, with
   the basis off the edge and the label wrapped to three lines to make room for it. The figure
   column takes exactly what its widest number needs, because it never wraps, and the label takes
   the rest, which is where the prose belongs. Nothing here scrolls sideways. */
.factlist th[scope="row"] { white-space: normal; vertical-align: top; }
.factlist td { vertical-align: top; }
.row-note { display: block; font-weight: 400; font-size: 13px; line-height: 1.35;
  color: var(--on-surface-variant); white-space: normal; margin-top: 2px; }
.compare-page { padding-bottom: var(--xl); }
.compare-page .page-head { max-width: 850px; }
/* The table sizes its own columns instead of scrolling sideways inside a box. `position: sticky`
   resolves against the nearest scrollport, so the horizontal scroller this table used to sit in
   pinned the header to a box that never scrolls vertically, which is to say not at all. */
.compare-page .table-scroll { overflow: visible; }
.compare-table { min-width: 0; table-layout: fixed; }
.compare-table th, .compare-table td { min-width: 0; text-align: left; vertical-align: top;
  white-space: normal; }
.compare-table th[scope="row"] { width: 19%; }
.compare-table thead a { display: block; color: var(--on-surface); text-decoration: none; }
.compare-table thead a:hover { color: var(--secondary); text-decoration: underline; }
.compare-basis { margin: var(--sm) 0 0; max-width: 900px; }
/* The phone's key strip. Nothing at all above the breakpoint that switches it on, where the
   sticky header row below already keeps the picture and the price on screen. The caption is
   still in the flow for a screen reader, so it takes no room until it has something to show:
   `.datatable`'s own padding left an 8 px gap above the table with nothing in it. */
.compare-key { display: none; }
.compare-table caption { padding: 0; }
/* The header travels with the reader. Fifteen rows down, a column is a number again unless the
   picture and the price are still on screen, which is the whole job of a comparison. It clears
   the site header above it, and sits under it where the two meet. */
@media (min-width: 721px) and (min-height: 600px) {
  .compare-table thead th { position: sticky; top: 65px; z-index: 3; padding-top: 8px;
    background: var(--surface-container-lowest); box-shadow: inset 0 -2px 0 var(--surface-container-highest); }
}
/* The header cell is where the photograph-led buyer meets these records, so the picture is a
   picture rather than a 140 px thumbnail over a line of grey text. */
.compare-head-bar { display: flex; align-items: center; justify-content: space-between; gap: 8px;
  min-height: 44px; }
.compare-cover { display: block; width: 100%; height: 120px; object-fit: cover;
  border-radius: var(--r-lg); background: var(--surface-container); }
.compare-head-text { padding-top: 8px; }
.compare-col-n { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px;
  border-radius: 50%; background: var(--primary-container); color: #fff; font-size: 13px; }
.compare-price { font-size: 20px; font-weight: 600; color: var(--on-surface); }
/* Two lines, because the header row stays on screen and a five-line title would take a fifth of
   it. The record's own page carries the whole title. */
.compare-title { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden; margin-top: 2px; }
.compare-where { display: block; }
/* Above the picture, not floating over its top corner: half on the image and half off it, the
   circle read as a mistake wherever the photograph behind it happened to be light. A named
   control in the header row says what it does and keeps the 44 px target the design rules ask
   for without a disc drawn around a glyph. */
.compare-drop { display: inline-flex; align-items: center; gap: 4px; min-height: 44px; padding: 0 10px;
  border-radius: var(--r-xl); color: var(--on-surface-variant); font-size: 13px; text-decoration: none; }
.compare-drop:hover { color: var(--error); background: var(--surface-container); }
.compare-drop svg { width: 16px; height: 16px; }
/* One radio group per column, the slides stacked, each slide carrying its own pair of arrows.
   Same mechanism as the record's own gallery, without the thumbnails or the captions. */
.cmp-track { display: grid; border-radius: var(--r-lg); overflow: hidden; }
.cmp-slide { grid-area: 1 / 1; position: relative; visibility: hidden; }
.cmp-open { position: absolute; inset: 0; }
.cmp-nav { position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: space-between; pointer-events: none; }
.cmp-arrow { pointer-events: auto; cursor: pointer; display: grid; place-items: center;
  width: 36px; height: 44px; color: #fff; background: rgba(0, 0, 0, .45); }
.cmp-arrow:first-child { border-radius: 0 var(--r-lg) var(--r-lg) 0; }
.cmp-arrow:last-child { border-radius: var(--r-lg) 0 0 var(--r-lg); }
.cmp-arrow:hover { background: rgba(0, 0, 0, .7); }
.cmp-arrow svg { width: 20px; height: 20px; }
.cmp-count { position: absolute; right: 6px; bottom: 6px; padding: 1px 6px; border-radius: var(--r);
  background: rgba(0, 0, 0, .55); color: #fff; font-size: 12px; }
.gal-input:focus-visible ~ .cmp-track, .cmp-arrow:focus-visible { outline: 2px solid var(--secondary); outline-offset: 2px; }
/* One glyph per row, so a reader finds the hospital line by shape before they read a word of
   it. The place rows carry the map's own coloured symbol for that kind; everything else is a
   mono glyph in the label's colour, which is the same split the property page already makes
   between what is on the map and what is on the record. */
.compare-table th[scope="row"] .ico, .compare-table th[scope="row"] .poi-ico {
  width: 16px; height: 16px; vertical-align: -3px; margin-right: 6px; }
.compare-table th[scope="row"] .ico { color: var(--outline); }
/* Rows where every column answers the same are the ones a grid exists to let a reader skip. */
.compare-table tr.same th[scope="row"], .compare-table tr.same td { color: var(--on-surface-variant); }
.compare-table tr.same .poi-ico { opacity: .5; }
/* Keeping a row at the top. The control sits at the end of its label rather than in a column of
   its own, so no width is taken from the values on a table that is already wide, and it is the
   44 px target every icon-only control here gets. Faint until the row is hovered or the button
   is focused, then legible: fifteen dark pins down the label column would read as fifteen
   controls demanding attention on a page whose job is to be read. Once a row is pinned its
   button stays visible, because that is the state, not an offer. */
.pin-form { display: inline; }
.pin { display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px;
  margin: -13px 0 -13px 2px; padding: 0; border: 0; background: none; cursor: pointer;
  color: var(--outline); opacity: .5; transition: opacity .12s ease, color .12s ease; }
.pin svg { width: 16px; height: 16px; }
/* Visible at rest, not on hover alone: at zero the budget buyer could not find the control on a
   laptop at all and only aimed at it because the brief said it was there. A control nobody can
   see is not a control, so it is quiet rather than hidden, and it comes up to full weight the
   moment the row is hovered or the button is focused. */
.compare-table tr:hover .pin, .pin:focus-visible, .pin-on { opacity: 1; }
.pin:hover { color: var(--secondary); }
.pin-on { color: var(--secondary); }
.pin:focus-visible { outline: 2px solid var(--secondary); outline-offset: -6px; border-radius: 50%; }
/* What the dimming used to say on a row every column answers the same, said in words instead,
   because a pinned row is never dimmed. Text, so it survives at any contrast and reads aloud. */
.row-same { display: inline-block; margin-left: 8px; padding: 1px 6px; border-radius: var(--r);
  background: var(--surface-container-high); color: var(--on-surface-variant);
  font: 500 11px var(--sans); letter-spacing: .04em; text-transform: uppercase; }
/* The note that says where the kept rows live, and the one press that drops them all. */
.pin-note { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sm);
  justify-content: space-between; }
.pin-note p { margin: 0; }
/* A pinned row is at the top and looks it. The bar is on the label cell, which is the sticky
   one, so it stays beside the row as the table scrolls sideways. The row keeps full contrast:
   `tr.pinned` is never given the dimming `tr.same` carries, because the row a reader called
   most important must not be the faintest thing on the page just because two properties agree.
   Position is the signal and the solid pin is the second one; the tint is neither on its own. */
.compare-table tr.pinned th[scope="row"] { box-shadow: inset 3px 0 0 var(--secondary); padding-left: 13px; }
.compare-table tr.pinned th[scope="row"], .compare-table tr.pinned td { background: var(--surface-container-low); }
.compare-table tr.pinned + tr:not(.pinned) th[scope="row"], .compare-table tr.pinned + tr:not(.pinned) td {
  border-top: 2px solid var(--outline-variant); }
/* One column leads a row on a fact that has a direction: the lower rate, the shorter distance,
   the quicker drive. The word is text and the tint is a second cue, never the only one. It is
   deliberately not the green of `.delta-below`: that green means "under the median for this
   type" on the search cards and again in the row of pills two lines below this one, and three
   buyers in a row read a green cell here as the same claim. One tint, one meaning. */
.compare-table td.lead { background: var(--surface-container); }
.lead-tag { display: block; margin-top: 2px; font: 500 11px var(--sans); letter-spacing: .06em;
  text-transform: uppercase; color: var(--secondary); }
.compare-table tr.compare-price-row td { font-size: 18px; font-weight: 600; }
.compare-table tr.compare-actions td { display: table-cell; }
.compare-table tr.compare-actions .fav, .compare-table tr.compare-actions .fav-form { margin-top: 8px; }
/* The phone. A 1050 px table in a 293 px window showed about 120 px of one property, clipped
   mid-word, and no scroll position held two at once, so the page compared nothing. Below this
   width each row becomes a labelled block whose cells sit two to a line: two properties are
   readable together, every field the desktop table shows is still here, and it is the same
   markup rather than a second copy of it in the DOM. */
@media (max-width: 720px) {
  .compare-table { min-width: 0; }
  .compare-table, .compare-table tbody, .compare-table tr, .compare-table td { display: block; width: 100%; }
  .compare-table thead th:first-child { display: none; }
  .compare-table thead { display: block; }
  .compare-table thead tr { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sm); }
  .compare-table thead th { min-width: 0; border-bottom: 0; }
  .compare-cover { height: 110px; }
  .compare-drop span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
  .compare-table tbody tr { padding: 12px 0; border-bottom: 1px solid var(--outline-variant); }
  .compare-table tbody tr:last-child { border-bottom: 0; }
  /* `width: 19%` is the desktop label column and it was never taken back here, so every label
     was 59 px wide on a 390 px screen and read one word per line: "MEDIAN / FOR / THIS / TYPE". */
  .compare-table th[scope="row"] { display: block; position: static; width: auto; min-width: 0; padding: 0 0 6px;
    border: 0; color: var(--on-surface-variant); font-size: 13px; text-transform: uppercase; letter-spacing: .04em; }
  .compare-table tbody tr { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px var(--sm); }
  .compare-table th[scope="row"] { grid-column: 1 / -1; }
  /* No hover on a phone, so the control is always visible, and the label block is short enough
     to carry it without crowding the values under it. */
  .pin { opacity: 1; margin: -13px 0; }
  .compare-table tr.pinned th[scope="row"] { padding-left: 11px; }
  .compare-table td { min-width: 0; border: 0; padding: 0 0 4px; }
  /* Which column a value belongs to, matching the numbered header cards above. Colour is never
     the only signal here and neither is position: the number is text. */
  .compare-table td::before { content: attr(data-col); }
  .compare-table td::before, .key-n { display: inline-flex; align-items: center;
    justify-content: center; width: 18px; height: 18px; margin-right: 6px; border-radius: 50%;
    background: var(--surface-container-high); color: var(--on-surface-variant); font-size: 12px; }
  /* The header cards scroll away at this width and the sticky header above is desktop-only, so
     fifteen rows down the numbered badge on a cell was pointing at cards nobody could see. The
     caption already sits above this table and holds its place while the table moves, so it
     carries the same identities in one line: the badge, a short price, the village. */
  .compare-table caption { display: block; position: sticky; top: 65px; z-index: 4;
    padding: 8px 0; background: var(--surface-container-lowest);
    box-shadow: inset 0 -1px 0 var(--outline-variant); }
  .compare-key { display: block; font-size: 13px; line-height: 1.7;
    color: var(--on-surface-variant); white-space: normal; }
  /* One reading per property. A price wrapping away from its own village number reads as five
     properties on a strip that names four. */
  .key-item { white-space: nowrap; }
  /* The position marker is a pill with a fixed 44 px bar and a phrase beside it, which is wider
     than half a 375 px screen. It wraps here rather than being clipped mid-word. */
  .compare-table .delta { white-space: normal; flex-wrap: wrap; padding: 4px 8px; }
}

.note { background: var(--surface-container-low); border-left: 3px solid var(--secondary); border-radius: var(--r-lg); padding: 12px var(--sm); font-size: 14px; color: var(--on-surface-variant); }
.note strong { color: var(--on-surface); }

/* Present to a screen reader and to a crawler, absent from the page. Not display:none, which
   removes it from both. Carries the name of every icon-only control on the site. */
.vh { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
      clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; }

/* Caveats and methodology stay in the HTML for crawlers, folded behind an icon for readers.
   The control is an icon, so its 44px target is the hit area and the glyph inside is 18px. */
.disclose { display: inline-flex; align-items: center; justify-content: center; vertical-align: middle;
  width: 44px; height: 44px; padding: 0; margin: 0; border: 0; background: none;
  color: var(--secondary); cursor: pointer; border-radius: 50%; }
.disclose:hover { color: var(--primary); background: var(--surface-container); }
/* Open is a state, and colour is never the only cue for one: the icon also gains a ring. */
.disclose:has(+ .disclose-body:popover-open) { color: var(--primary); background: var(--surface-container);
  box-shadow: inset 0 0 0 1px var(--outline-variant); }
/* Opening must not change the icon's own box. A popover is in the top layer, so it is out of
   flow entirely: a caveat can open from a heading row, a right-aligned price, or the map caption
   bar without moving anything, and it is clipped by nothing, not by the column it was rendered
   in, not by the map frame, not by a table that scrolls sideways.
   The rules below are the fallback for a browser without anchor positioning: centred at the
   bottom of the window, which is where every panel used to open. */
.disclose-body { position: fixed; z-index: 60; inset: auto; left: 50%; bottom: var(--md); transform: translateX(-50%);
  width: min(440px, calc(100vw - var(--md))); max-height: min(60vh, 520px); overflow-y: auto;
  background: var(--surface-container-lowest); border: 1px solid var(--outline-variant);
  border-radius: var(--r-xl); padding: var(--md); margin: 0; box-shadow: 0 10px 30px rgba(0, 0, 0, .18);
  font-size: 14px; line-height: 1.7; color: var(--on-surface-variant); }
/* The panel belongs beside the icon that opened it, not at the foot of the window: a caveat
   read half a screen away from the number it qualifies is a caveat the reader has to match up
   by hand. Anchored below the icon and aligned to its left edge, with the built-in fallbacks
   flipping it above or across when the edge of the window is nearer than the panel is wide.
   The anchor is named per pair in an inline style, because disclose() is the only place that
   knows which icon owns which panel: the implicit popovertarget anchor does not resolve in
   Chrome, and one shared name cannot work, since these icons are emitted in a different order
   from the one they appear in, so tree order picks the wrong icon. */
@supports (top: anchor(bottom)) {
  .disclose-body { top: anchor(bottom); left: anchor(left); right: auto; bottom: auto;
    transform: none; margin: var(--xs) 0 0;
    width: min(360px, calc(100vw - var(--md)));
    /* An icon in the middle of the window has half of it to open into, less its own height and
       the two gaps, and flip-block sends the panel to whichever half is larger. Cap the panel
       at that worst case and it always lands on screen whole; the longest of them, methodology,
       then scrolls inside itself rather than hanging its last hundred pixels off the bottom.
       This is why the height is capped here and not in the rule above: the unanchored fallback
       opens at the foot of the window, where the full 60vh fits. */
    max-height: calc(50vh - 60px);
    position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline; }
  /* On a phone the panel is as wide as the screen, so neither of the icon's edges is a place it
     can align to: flipping it left or right only moves which side hangs off. Centre it and keep
     the anchoring the reader actually asked for, the vertical one, directly under the icon. */
  @media (max-width: 600px) {
    .disclose-body { left: 50%; right: auto; transform: translateX(-50%);
      position-try-fallbacks: flip-block; }
  }
}
.disclose-body > * { margin: 0 0 var(--xs); }
.disclose-body > :last-child { margin-bottom: 0; }
/* The panel names itself. The label is a tooltip on the icon, which a touch reader never sees. */
.disclose-title { font: 600 14px var(--sans); color: var(--on-surface); }
.disclose .datalist { margin-bottom: var(--xs); }
/* Esc and a click outside already close a popover, but neither is visible. The X is. Drawn at
   its glyph size with a transparent 44px target centred on it, the same trick the map caption
   bar uses, so the corner stays small without the hit area shrinking with it. Floated and stuck
   to the top: the two longest panels scroll inside themselves, and a close button that scrolls
   away is missing from exactly the panels that most need one. */
.disclose-close { position: sticky; top: 0; float: right; display: inline-flex;
  width: 24px; height: 24px; margin-left: var(--xs); padding: 0; border: 0; background: none;
  border-radius: 50%; color: var(--on-surface-variant); cursor: pointer; }
.disclose-close::before { content: ""; position: absolute; top: 50%; left: 50%;
  width: 44px; height: 44px; transform: translate(-50%, -50%); }
.disclose-close:hover { color: var(--primary); background: var(--surface-container); }

/* A heading and the icons explaining it, on one line. */
.head-row { display: flex; align-items: center; gap: var(--base); flex-wrap: wrap; }

/* Price position. The gradient is a secondary cue only: every point on the scale is also
   labelled in words and euros, and the disclosure says a lower position is not a better buy.
   Olive to terracotta, the design system's own pair, and terracotta is used here for the same
   reason it is used for a reduction: it marks the top of a range, it does not judge a house. */
.scale { position: relative; height: 80px; margin-bottom: 4px; }
.scale-track { position: absolute; left: 0; right: 0; top: 40px; height: 12px; border-radius: 999px;
  background: linear-gradient(90deg, #65755c 0%, #d9e4ed 50%, #b96a4a 100%); }
/* Two marks, two shapes: the median is a dashed reference line, this property is a solid dot on
   the track. Same colour family at different weights read as the same thing at a glance, and on a
   skewed range the two marks can land millimetres apart. */
.scale-median, .scale-here { position: absolute; top: 32px; width: 0; }
.scale-median::before, .scale-here::before { content: ""; position: absolute; }
.scale-median::before { top: 2px; left: -1px; width: 2px; height: 26px;
  background: repeating-linear-gradient(180deg, var(--outline) 0 4px, transparent 4px 7px); }
.scale-here { z-index: 1; }
.scale-here::before { top: 5px; left: -9px; width: 18px; height: 18px; border-radius: 999px;
  background: var(--primary); box-shadow: 0 0 0 3px var(--surface-container-lowest), 0 1px 3px rgba(0, 0, 0, .28); }
.scale-median span, .scale-here span { position: absolute; left: 0; transform: translateX(-50%); white-space: nowrap;
  font: 500 13px/1 var(--sans); font-variant-numeric: tabular-nums; }
.scale-median span { top: 36px; color: var(--on-surface-variant); }
.scale-here span { top: -26px; color: var(--primary); font-weight: 600; }
.scale-here span em { display: block; margin-top: 4px; font: 600 12px/1.2 var(--sans); font-style: normal; }
.scale-left span { transform: translateX(0); }
.scale-right span { transform: translateX(-100%); }
.scale-ends { display: flex; justify-content: space-between; margin-top: var(--md); }
.scale-full { margin: var(--sm) 0 0; }

/* ------------------------------------------------------------------- detail */
.detail-head { display: flex; justify-content: space-between; align-items: flex-end; gap: var(--md); flex-wrap: wrap; padding: var(--md) 0 var(--md); }
/* The size facts, above the photographs. A plain wrapping row: on a narrow screen the items
   reflow rather than scroll sideways, so nothing is hidden off the edge. */
.glance { display: flex; flex-wrap: wrap; align-items: center; gap: 10px var(--md);
  border-top: 1px solid var(--outline-variant); border-bottom: 1px solid var(--outline-variant);
  padding: 12px 0; margin-bottom: var(--md); }
.glance-item { display: inline-flex; align-items: center; gap: 8px; }
.glance-item .ico { color: var(--secondary); flex: none; }
/* A glance item that is a link keeps the row's look; the underline on its value says it goes
   somewhere, and the target panel carries the measurement's basis. The vertical padding is the
   touch target, not decoration: the text row alone is 24px against the 44px rule. */
a.glance-item { color: inherit; text-decoration: none; padding: 12px 0; margin: -12px 0; }
a.glance-item .data-mono { text-decoration: underline; text-underline-offset: 3px; }
/* One row of plain anchors under the strip: the page runs to several screens and each
   section is one tap away. Wraps on a phone, nothing scrolls sideways. The padding makes each
   link a 44px target; the negative margin keeps the row's visual height at one line of text. */
.jump { display: flex; flex-wrap: wrap; gap: 0 16px; margin-bottom: var(--md); }
/* 44 px in both directions, not only in height: "Map" and "Price" are short words and
   were 28 and 32 px wide, which is a target smaller than the rule asks for. */
.jump a { color: var(--secondary); text-decoration: none; display: inline-flex;
  align-items: center; justify-content: center; min-width: 44px; padding: 12px 0; }
.jump a:hover, .jump a:focus-visible { text-decoration: underline; }
/* The locality verdict under the price: the .rate-vs inside carries the words and the muted
   colour; the underline says it lands on the section with the full basis. */
.verdict-link { display: block; text-decoration: none; padding: 13px 0; margin: -9px 0; }
.verdict-link .rate-vs { text-decoration: underline; text-underline-offset: 3px; }
.detail-price { font: 600 32px/1.3 var(--serif); color: var(--primary); font-variant-numeric: tabular-nums; text-align: right; }
/* Media strip. A radio group and stacked slides, so it browses with the mouse, the keyboard or
   scripting off. Nothing here needs JavaScript, and picking a thumbnail swaps the slide in
   place instead of jumping to a fragment, so the page never scrolls under the reader. */
.gallery-strip { margin-bottom: var(--lg); }
/* Invisible but still in flow and still focusable: display:none would lose the arrow keys, and
   taking the radios out of flow made the browser scroll the page when focus landed on one. */
.gal-input { width: 0; height: 0; margin: 0; opacity: 0; }
/* Every slide in the one grid cell; the checked one is revealed by the rules the page emits. */
.gal-track { display: grid; border-radius: var(--r-xl); overflow: hidden; background: var(--surface-container); }
/* Column so a one-line caption still fills the cell a two-line one sized: stacked slides share
   one grid track, and a short slide would otherwise leave a bare strip under its caption. */
.gal-slide { grid-area: 1 / 1; margin: 0; position: relative; visibility: hidden; display: flex; flex-direction: column; }
.gal-slide img, .gal-slide video { display: block; width: 100%; height: 520px; object-fit: cover; background: var(--surface-container); }
.gal-slide figcaption { flex: 1; padding: 12px var(--sm); font-size: 13px; color: var(--on-surface-variant);
  background: var(--surface-container-lowest); border-top: 1px solid var(--outline-variant); }
.gal-thumbs { display: flex; gap: 8px; overflow-x: auto; padding: 8px 0 0; }
.gal-thumb { position: relative; flex: none; cursor: pointer; width: 96px; height: 64px; border-radius: var(--r-xl); overflow: hidden;
  border: 2px solid transparent; outline-offset: 2px; }
.gal-thumb img { width: 100%; height: 100%; object-fit: cover; }
.gal-thumb:hover, .gal-thumb:focus-visible { border-color: var(--secondary); }
.gal-play { position: absolute; inset: 0; display: grid; place-items: center; color: #fff; background: rgba(0, 0, 0, .35); }
/* Overlay arrows. They are labels, so a click both moves the slide and puts the focus on the
   radio behind it, which is what hands the left/right keys to the browser's radio navigation. */
.gal-nav { position: absolute; top: 0; left: 0; right: 0; height: 520px; display: flex;
  align-items: center; justify-content: space-between; padding: 0 var(--sm); pointer-events: none; }
.gal-arrow { pointer-events: auto; cursor: pointer; display: grid; place-items: center;
  width: 44px; height: 44px; border-radius: 50%; color: #fff; background: rgba(0, 0, 0, .45); }
.gal-arrow:hover { background: rgba(0, 0, 0, .7); }
@media (max-width: 780px) { .gal-nav { height: 260px; } }
.gal-note { margin: 8px 0 0; }
.gallery { display: grid; grid-template-columns: 3fr 1fr; grid-template-rows: 1fr 1fr; gap: var(--xs); height: 520px; margin-bottom: var(--lg); }
.gallery > :first-child { grid-row: span 2; }
.gallery figure { margin: 0; position: relative; border: 1px solid var(--outline-variant); border-radius: var(--r-lg); overflow: hidden; background: var(--surface-container); }
.gallery img { width: 100%; height: 100%; object-fit: cover; position: absolute; inset: 0; }
.detail-grid { display: grid; grid-template-columns: minmax(0, 2fr) minmax(300px, 1fr); gap: var(--md); align-items: start; padding-bottom: var(--xl); }
/* A sticky column taller than the window pins its own top and hides the rest of itself until
   the page ends. Capping it to the window keeps every panel reachable whatever goes in here. */
.detail-aside { position: sticky; top: 88px; max-height: calc(100vh - 104px); overflow-y: auto; }
/* Written for the property page, where the column it sits in carries the padding. The results
   column pads each child instead, so the village figures panel and its heading landed hard
   against the left edge of the phone. */
.split-results > .detail-section { padding: 0 var(--md); }
.detail-section { margin-bottom: var(--lg); }
.detail-section > h2 { margin-bottom: var(--sm); }
/* On the map, not on the frame, for the reason given at .island-map .map-canvas below: the frame
   holds the key underneath now and has to be free to grow by it, or the frame's own overflow
   clips the key off the bottom. flex:none so the map keeps this height rather than being shrunk
   to fit a frame that was sized before the key was under it. */
.detail-section .map-canvas { height: 380px; flex: none; }
/* Nearby-place layers. The per-layer show/hide rules are emitted next to the map itself,
   because the categories come from the data, not from this file. */
.poi-hint { margin: 0 0 8px; }
.poi-input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.poi-picker { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.poi-chip { display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 0 14px; cursor: pointer;
  border: 1px solid var(--outline-variant); border-radius: 999px; background: var(--surface);
  font: 500 13px/1 var(--sans); color: var(--on-surface-variant); }
.poi-chip:hover { border-color: var(--poi, var(--secondary)); }
.poi-chip .poi-ico, .poi-chip .ico { width: 18px; height: 18px; flex: none; }
.poi-chip-all { border-style: dashed; }
/* One colour per kind. The glyph carries the meaning; colour is a second cue, never the only
   one, so a colour-blind reader loses nothing. */
.poi-beach { --poi: #2f6f89; }
.poi-supermarket { --poi: #65755c; }
.poi-pharmacy { --poi: #b96a4a; }
.poi-healthcare { --poi: #a2483c; }
.poi-school { --poi: #123b4a; }
.poi-bus_stop { --poi: #58636b; }
.poi-bank { --poi: #23657f; }
.poi-fuel { --poi: #7d6a3f; }
.poi-airport { --poi: #3f5d6b; }
.poi-ferry { --poi: #1f6f6a; }
.poi-hospital { --poi: #8c3a4e; }
.poi-marina { --poi: #2c5f7a; }
.poi-town { --poi: #4a4f63; }
.poi-golf { --poi: #4f7a4a; }
.poi-ico { fill: var(--poi, var(--secondary)); }
/* The glyph sprite: referenced by <use>, never drawn where it sits. */
/* visibility stays visible on purpose: `.map-frame-live > svg` hides the drawn map once the
   street map paints, and a hidden sprite can take its <use> instances down with it. */
.map-frame svg.poi-defs { position: absolute; width: 0; height: 0; overflow: hidden; visibility: visible; }
/* Nearby places are off until asked for: the chips above the map are the switch. */
.poi-map .map-poi, .poi-map .map-poi-live { visibility: hidden; }
.datatable th .poi-ico { vertical-align: -4px; margin-right: 6px; }
/* How wide the position is actually known. Drawn under the marker, never as a pin. */
.map-area { fill: var(--secondary); fill-opacity: .14; stroke: var(--secondary); stroke-opacity: .5; stroke-width: 2; stroke-dasharray: 6 6; }
/* A distance ring is an outline and never a fill: the fill above says "somewhere in here",
   which is the one thing a ring around an exact pin must not be read as saying. */
.map-ring { fill: none; stroke: var(--secondary); stroke-opacity: .45; stroke-width: 2; stroke-dasharray: 10 8; }
.map-poi .poi-disc { fill: var(--poi, var(--secondary)); stroke: #fff; stroke-width: 2; }
.map-poi use { fill: #fff; }
.map-poi-live { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px;
  border-radius: 50%; border: 2px solid #fff; background: var(--poi, var(--secondary));
  box-shadow: 0 1px 3px rgba(0, 0, 0, .35); }
.map-poi-live svg { width: 14px; height: 14px; fill: #fff; }
/* Corfu is a tall, narrow island: the overview map needs the height to show it whole. The height
   is on the map rather than on the frame, because the frame now also holds the key underneath:
   sized on the frame, the key ate 172px of a 440px phone frame and left the island 266px, which
   is a smaller map than the thing explaining it. The frame grows by whatever the key needs and
   the island keeps the height it was drawn for, at every width. The split view sets no height
   here on purpose, its frame is bounded by the viewport and the map flexes into what is left. */
.island-map .map-canvas { height: 560px; flex: none; }
.prose { max-width: 68ch; }
.prose h2 { margin: var(--lg) 0 var(--xs); font-size: 28px; }
.prose p { color: var(--on-surface-variant); line-height: 1.7; }
.breadcrumb { display: flex; flex-wrap: wrap; gap: var(--xs); padding: var(--sm) 0 0; font-size: 14px; color: var(--on-surface-variant); }
.breadcrumb a { color: var(--on-surface-variant); }

.page-head { padding: var(--lg) 0 var(--md); max-width: 760px; }
.page-head h1 { margin: var(--xs) 0 var(--sm); }

/* The island answer, folded. The summary is the price answer and stays visible; the rest of the
   passage is one click away and still in the raw HTML, which is all the citation rule asks. The
   marker is a caret we draw, because the native triangle sits on the first line of a sentence
   that wraps to two and reads as a bullet. */
.answer-block { max-width: 70ch; margin: var(--sm) 0; }
.answer-block > summary {
  display: flex; gap: 10px; align-items: baseline; cursor: pointer; list-style: none;
  padding: 10px 0; font: 400 17px/1.55 var(--sans); color: var(--on-surface);
}
.answer-block > summary::-webkit-details-marker { display: none; }
.answer-block > summary::before {
  content: ""; flex: none; width: 7px; height: 7px; margin-top: 8px;
  border-right: 2px solid var(--secondary); border-bottom: 2px solid var(--secondary);
  transform: rotate(-45deg); transition: transform .15s;
}
.answer-block[open] > summary::before { transform: rotate(45deg); }
.answer-block > summary:hover { color: var(--secondary); }
.answer-block > p { margin: 0 0 var(--xs) 17px; color: var(--on-surface-variant); font-size: 15px; }
@media (prefers-reduced-motion: reduce) { .answer-block > summary::before { transition: none; } }

/* --------------------------------------------------------------------- about */
.about-hero { padding-top: var(--xl); padding-bottom: var(--lg); }
.about-actions { display: flex; gap: var(--xs); flex-wrap: wrap; margin-top: var(--md); }
.about-story { padding: var(--lg); margin: var(--lg) 0 var(--xl); }
.about-story h2 { margin: var(--xs) 0 var(--md); }
.about-story p { color: var(--on-surface-variant); line-height: 1.7; }
.about-how { display: grid; grid-template-columns: 1fr 1fr; gap: var(--lg); align-items: center; }
.about-how-copy h2 { margin: var(--xs) 0 var(--sm); }
.about-steps { list-style: none; padding: 0; margin: 0; counter-reset: about-step; }
.about-steps li { counter-increment: about-step; display: grid; grid-template-columns: 34px 1fr; gap: var(--sm); padding: var(--sm) 0; border-top: 1px solid var(--outline-variant); }
.about-steps li::before { content: counter(about-step, decimal-leading-zero); color: var(--secondary); font: 600 14px/1 var(--sans); padding-top: 3px; }
.about-steps strong, .about-steps span { grid-column: 2; }
.about-steps strong { font: 600 20px/1.3 var(--serif); color: var(--primary); }
.about-steps span { color: var(--on-surface-variant); }
.asklist { list-style: none; padding: 0; margin: 0; counter-reset: ask; }
.asklist li { counter-increment: ask; display: grid; grid-template-columns: 26px 1fr; gap: var(--sm); padding: var(--sm) 0; border-top: 1px solid var(--outline-variant); color: var(--on-surface); }
.asklist li:first-child { border-top: 0; padding-top: 0; }
.asklist li::before { content: counter(ask); color: var(--secondary); font: 600 14px/1.5 var(--sans); }
.about-boundary { margin-bottom: var(--xl); background: var(--surface-container-low); }
.about-boundary h2 { margin: var(--xs) 0; }
.about-boundary p { max-width: 70ch; }
.about-methodology { padding-bottom: var(--xl); }

/* ------------------------------------------------------------------- footer */
.site-footer { margin-top: auto; background: var(--primary); color: var(--on-primary-container); padding: var(--lg) 0; }
.site-footer .shell { display: flex; justify-content: space-between; gap: var(--md); flex-wrap: wrap; align-items: flex-start; }
.site-footer .brand { color: #fff; }
.site-footer p { margin: var(--xs) 0 0; font-size: 14px; max-width: 280px; }
.footer-links { display: flex; gap: var(--md); flex-wrap: wrap; font-size: 14px; }
.footer-links a, .topnav nav a { display: inline-flex; align-items: center; min-height: 44px; }
.site-footer a { color: var(--on-primary-container); text-decoration: none; }
.site-footer a:hover { color: #fff; text-decoration: underline; }
.site-footer small { display: block; font-size: 12px; color: #7aa3b6; }

.empty { text-align: center; padding: var(--xl) var(--md); }
.error-page { padding-block: var(--xl); text-align: center; }
.error-page h1 { font: 600 48px/1.1 var(--serif); color: var(--primary); margin-bottom: var(--sm); }

/* Native popover dialog: a button opens it, its own buttons, Esc, or a click outside close it.
   The dim is ::backdrop rather than the element itself, so clicking the dim lands outside the
   popover, which is what makes the browser's own light dismiss fire. */
/* Written as .slab.leave-modal so it outranks `.slab + .slab`, which would otherwise win and
   replace the auto margins that centre the popover in the viewport. */
.slab.leave-modal { max-width: 460px; width: calc(100% - 32px); max-height: calc(100% - 32px); margin: auto; overflow: auto; text-align: center; }
.leave-modal::backdrop { background: rgba(18, 59, 74, .55); }

/* --------------------------------------------------------------- responsive */
@media (max-width: 1100px) {
  .hero { grid-template-columns: 1fr; }
  .island-map .map-canvas { height: 440px; flex: none; }
  .split { grid-template-columns: 1fr; }
  .split-results { border-right: 0; }
  /* No `order: -1` here. It used to pull the map above the results, which put the map first on
     screen while the results, and the page's only <h1>, still came first in the document: tab
     order and reading order then disagreed, and a keyboard or screen-reader user met the whole
     result list before the map they were shown first. Source order wins, so the heading that
     names the result set is what a reader meets, and the map follows it. */
  .split-map { position: relative; top: 0; height: 460px; }
  /* Back in the flow once the columns stack: this list is the way to the records when the map
     is only a picture, and out of flow it would sit under whatever follows it. */
  .split-map .map-places { position: static; }
  /* The toggle belongs wherever the split view has collapsed to one column, not only on a
     phone. Between 781 and 1100px the layout was already stacked, the `view-map` class was
     already on the wrapper, and none of the rules that act on it applied: there was no control
     to switch with and, with the map no longer pulled up by `order`, no map above the fold
     either. One breakpoint owns the stacked layout now. */
  /* One class deeper than the rule that hides it, which sits further down this file: at equal
     specificity the later rule won and the Map/List switch was hidden at every width, so the
     phone had no way to reach the map at all. */
  .filterbar-row .segmented { display: inline-flex; }
  /* The order goes with them: it reorders a list this view is not showing, and on a phone it
     costs two rows between the heading and the map. */
  .view-map .results-list, .view-map .sortbar,
  .view-map .results-showing, .view-map .compare-bar, .view-map .pager { display: none; }
  /* The village figures panel joins them, for the same reason and a worse one: it is 1638px of
     table on a 375px screen, so tapping Map put the map two and a half screens below the control
     that asked for it. It is the first thing under the heading in List view, which is the view a
     crawler and every default arrival get. */
  .view-map .split-results > .detail-section { display: none; }
  /* The order follows the filters in force rather than being pushed to the far edge: at this
     width there is no far edge to push it to, only a gap in the middle of a row. */
  .results-head { padding: 10px var(--sm); }
  .results-head .sortbar { margin-left: 0; }
  .view-map .split-map { height: auto; }
  .view-map .split-map .map-frame { height: calc(100vh - 210px); }
  /* Stacked, two frames share the panel's height rather than each taking it. */
  .view-map .split-map .island-maps { height: calc(100vh - 210px); }
  .view-map .split-map .island-map-frame .map-frame { height: auto; }
  .view-list .split-map { display: none; }
  .split-map .map-frame { border-top: 1px solid var(--outline-variant); }
  /* minmax(0, 1fr), not 1fr: an auto minimum lets a wide child (a fact table) stretch the
     column and push the whole page sideways instead of scrolling inside its own box. */
  .detail-grid { grid-template-columns: minmax(0, 1fr); }
  .detail-aside { position: relative; top: 0; max-height: none; overflow: visible; }
}

@media (max-width: 780px) {
  .shell { padding: 0 var(--margin-mobile); }
  /* Scoped to the header row, not to `.topnav nav`: the menu below holds the same nav and the
     same search box, and a rule matching every descendant would hide those too. */
  .topnav-left nav, .shell > .topnav-search, .shell > .topnav-account { display: none; }
  .topnav-menu { display: block; }
  .topnav-menu > summary {
    display: inline-flex; align-items: center; gap: var(--xs); min-height: 44px; padding: 0 var(--base);
    font: 600 15px var(--sans); color: var(--on-surface-variant); cursor: pointer; list-style: none;
  }
  .topnav-menu > summary::-webkit-details-marker { display: none; }
  .topnav-menu > summary:focus-visible { outline: 2px solid var(--secondary); outline-offset: 2px; }
  .topnav-menu[open] > summary { color: var(--secondary); }
  /* Positioned against `.topnav`, which is sticky and therefore already a containing block. */
  .topnav-menu-body {
    position: absolute; top: 100%; left: var(--margin-mobile); right: var(--margin-mobile); z-index: 60;
    display: flex; flex-direction: column; gap: var(--sm);
    background: var(--surface-container-lowest); border: 1px solid var(--outline-variant);
    border-radius: var(--r-xl); padding: var(--sm); box-shadow: 0 4px 20px rgba(29, 42, 48, .12);
  }
  /* gap: 0 overrides the header row's 24px, which stacked into 68px tall rows here. The 44px
     touch target is the row height and the divider is what separates one link from the next. */
  .topnav-menu-body nav { display: flex; flex-direction: column; gap: 0; }
  .topnav-menu-body nav a { padding: 0 var(--base); border-bottom: 1px solid var(--outline-variant); }
  .topnav-menu-body nav a:last-child { border-bottom: 0; }
  .topnav-menu-body nav a[aria-current="page"] { border-bottom-color: var(--secondary); }
  .topnav-menu-body .topnav-search input { width: 100%; }
  .topnav-menu-body .topnav-account { justify-content: center; }
  /* The phone's own rules for the key are gone: it is a strip under the map at every width now,
     so the overrides that made it one here, and the 50px this frame grew to buy back the island
     the strip was sitting on, are both answering a question that is no longer asked. */
  .display-lg { font-size: 36px; line-height: 1.2; }
  .section { padding-block: var(--lg); }
  .hero { padding-top: var(--md); gap: var(--md); }
  .field-row, .datalist, .bento { grid-template-columns: 1fr; }
  .bento { grid-template-rows: none; }
  .bento > :first-child { grid-row: auto; }
  .bento > :first-child.tile { min-height: 260px; }
  .panel-actions { flex-direction: column; }
  /* minmax(0, 1fr), not 1fr: the auto minimum is the card's own min-content width, which on the
     property page's "Similar records" strip is wider than a 375px column, and `overflow: hidden`
     on the card then cut the right-hand side of every price and title off. */
  .result-card { grid-template-columns: minmax(0, 1fr); }
  .result-media { height: 200px; }
  /* The outline shrinks on a phone; the card stays one row so the name and figures lead. */
  .island-card { grid-template-columns: 96px 1fr; }
  .island-card-outline { height: 110px; }
  .gal-slide img, .gal-slide video { height: 260px; }
  .gallery { grid-template-columns: 1fr; grid-template-rows: none; height: auto; }
  .gallery > :first-child { grid-row: auto; height: 260px; }
  .gallery figure:not(:first-child) { display: none; }
  .detail-price { text-align: left; }
  .filterbar { position: static; }
  /* Two to a row, not one: four stacked controls put the first result card under the fold on a
     phone, which is the wall this bar was slimmed down to remove. */
  .filterbar .field { flex: 1 1 calc(50% - 4px); }
  /* The pinned name column sizes itself to the longest name, and in the widest table on the
     island page that reached 319px inside a 293px window: the column that stays put covered
     every value scrolling under it, so the table showed its headings and none of its figures.
     Capped and allowed to wrap, so there is always more window than pinned column. Not the
     comparison table, which stops being a table at all below 720px and lays its own rows out. */
  .datatable:not(.compare-table):not(.factlist) th[scope="row"] { max-width: 40vw; white-space: normal; }
  .results-list { padding: var(--sm); }
  .split-results > .detail-section { padding: 0 var(--sm); }
  .filterbar-row { padding-block: 10px; }
  .filters > summary { display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 10px 18px; border: 1px solid var(--outline-variant); border-radius: 999px; font: 500 14px var(--sans); background: var(--surface-container-lowest); cursor: pointer; list-style: none; }
  .filters > summary::-webkit-details-marker { display: none; }
  .filters[open] > summary { background: var(--secondary); color: var(--on-secondary); border-color: var(--secondary); }
  /* The panel used to scroll inside its own box, capped at 44vh, so that an open one did not
     wall the reader off from the first result. Three buyers found what that cost on 17 August
     2026, and the cap is gone: a phone draws no scrollbar, so the clip landed on a field row and
     read as the end of the controls, with Apply and three more fields below it where nobody
     found them. Pinning Apply to the foot of the box was worse, not better, because a bar at the
     foot of a scroll box covers whatever is behind it: all three measured it landing across the
     Condition control, and a thumb aimed at the dropdown submitted the form instead. A commit
     control cannot overlay the controls it commits. The panel now flows with the page, so Apply
     sits at the end of the controls with nothing behind it and the page scrolls the way a page
     does. It is open only because the reader opened it, and applying closes it again. Full width
     because it ends the panel rather than joining the row of chips above it. */
  .filters[open] > form > .btn { flex: 0 0 100%; margin-top: 4px; }
  /* Open, the panel takes the row to itself. Sharing it with the map and list switch left the
     chips about 250px to scroll in, clipped hard against a control, which reads as a broken
     row rather than as one with more to the right. Closed, the two sit side by side as before. */
  .filterbar-row { flex-wrap: wrap; }
  .filters[open] { flex: 1 1 100%; }
  /* Six price bands wrap into four rows on a 375px screen. One row that scrolls sideways
     inside itself instead: a chip clipped at the edge reads as more to the right, which is
     what it is. */
  .filter-chips { flex-wrap: nowrap; overflow-x: auto; max-width: 100%; scrollbar-width: none; }
  .filter-chips::-webkit-scrollbar { display: none; }
  .filter-chips .chip, .filter-chips .label-caps { flex: none; white-space: nowrap; }
  /* Map view hides the card list and everything that only describes it: a result count, a
     pager and a note about the figures on cards are all furniture for a list that is not on
     screen, and with the map no longer pulled above them by `order` they sat between the
     heading and the map. The active filters stay: Section 18 asks for the count and the active
     filters to be visible, and this view has room for them now. */
  .about-hero { padding-top: var(--lg); }
  .about-how { grid-template-columns: 1fr; }
  .about-story { padding: var(--md); }
}

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
  .tile:hover img { transform: none; }
}

/* Price-pill markers (Stitch search map) */
.map-pill rect { fill: var(--primary); stroke: #fff; stroke-width: 4; }
.map-pill text { fill: #fff; font: 600 34px var(--sans); font-variant-numeric: tabular-nums; }
.map-pill-active rect { fill: var(--secondary); }

/* Hovering a result card marks its own record on the map. Size as well as colour, so the
   link between card and marker is not carried by colour alone. */
.map-pill.map-hi rect { fill: var(--secondary); stroke-width: 6; }
.map-marker.map-hi { fill: var(--secondary); r: 26; }
/* Lifted above its neighbours: on a crowded island a highlighted pin is usually under three
   others, and a highlight nobody can see is not one. */
.map-live-pin.map-hi { background: var(--secondary); z-index: 3; box-shadow: 0 0 0 4px rgba(47, 111, 137, .35), 0 1px 4px rgba(0, 37, 49, .3); }
/* On a banded map the highlight may not repaint the marker, because every colour there is a
   price and the hovered record would silently change bands. Size and a ring instead, which is
   what the highlight was already carrying alongside the colour. The band colour is restated
   here rather than moved, because these rules come later and would otherwise win it. */
.map-marker.map-hi[class*="map-band-"] { r: 26; stroke: var(--error); stroke-width: 5; }
.map-marker.map-hi.map-band-1 { fill: #a3e0fd; }
.map-marker.map-hi.map-band-2 { fill: var(--secondary); }
.map-marker.map-hi.map-band-3 { fill: var(--primary); }
.map-marker.map-hi.map-band-none { fill: var(--surface-variant); }
.map-marker.map-hi.map-band-mixed { fill: var(--surface-container-lowest); }
.map-pill.map-hi[class*="map-band-"] rect { stroke: var(--error); stroke-width: 7; }
.map-pill.map-hi.map-band-1 rect { fill: #a3e0fd; }
.map-pill.map-hi.map-band-2 rect { fill: var(--secondary); }
.map-pill.map-hi.map-band-3 rect { fill: var(--primary); }
.map-pill.map-hi.map-band-none rect { fill: var(--surface-variant); }
.map-pill.map-hi.map-band-mixed rect { fill: var(--surface-container-lowest); }
.map-live-pin[class*="map-band-"]:hover, .map-live-pin[class*="map-band-"]:focus-visible,
.map-live-pin.map-hi[class*="map-band-"] {
  border-color: var(--error);
  box-shadow: 0 0 0 4px rgba(186, 26, 26, .3), 0 1px 4px rgba(0, 37, 49, .3);
}
.map-live-pin.map-band-1:hover, .map-live-pin.map-band-1:focus-visible, .map-live-pin.map-hi.map-band-1 { background: #a3e0fd; }
.map-live-pin.map-band-2:hover, .map-live-pin.map-band-2:focus-visible, .map-live-pin.map-hi.map-band-2 { background: var(--secondary); }
.map-live-pin.map-band-3:hover, .map-live-pin.map-band-3:focus-visible, .map-live-pin.map-hi.map-band-3 { background: var(--primary); }
.map-live-pin.map-band-none:hover, .map-live-pin.map-band-none:focus-visible, .map-live-pin.map-hi.map-band-none { background: var(--surface-variant); }
.map-live-pin.map-band-mixed:hover, .map-live-pin.map-band-mixed:focus-visible, .map-live-pin.map-hi.map-band-mixed { background: var(--surface-container-lowest); }
@media (prefers-reduced-motion: no-preference) {
  .map-pill rect, .map-live-pin { transition: fill .15s ease, background .15s ease, box-shadow .15s ease; }
}

/* Mobile map/list toggle — desktop shows both panes, so the control is hidden there */
.segmented { display: none; border: 1px solid var(--outline-variant); border-radius: 999px; overflow: hidden; background: var(--surface-container-lowest); }
.segmented .seg { padding: 10px 18px; font: 500 14px var(--sans); color: var(--on-surface-variant); text-decoration: none; min-height: 44px; display: inline-flex; align-items: center; }
.segmented .seg-on { background: var(--secondary); color: var(--on-secondary); }

/* Shown wherever the split view has collapsed to one column; see the 1100px block. */

/* Left to the UA default, this landed at about 4.4:1 on the filter surfaces. */
::placeholder { color: var(--outline); opacity: 1; }

/* Save control. Present only where accounts are configured, and it is a form button, never a
   script. Saved is never carried by colour alone: the glyph fills, the word changes, and
   aria-pressed states it, so the three cues survive greyscale, high contrast and a screen
   reader alike. 44px tall, like every other control here. */
/* align-self, because a card body is a column flex container and a stretched control reads as
   the card's primary action rather than as a small toggle beside the facts. */
.fav-form { margin: 0; align-self: flex-start; }
.fav, .share-btn {
  display: inline-flex; align-items: center; gap: 6px; align-self: flex-start;
  min-height: 44px; padding: 10px 14px; border: 1px solid var(--outline-variant);
  border-radius: 999px; background: transparent; cursor: pointer;
  font: 500 14px var(--sans); color: var(--on-surface-variant); text-decoration: none;
  transition: background .2s, border-color .2s, color .2s;
}
.fav:hover, .share-btn:hover { border-color: var(--secondary); color: var(--secondary); background: var(--surface-container-low); }
.fav-on { border-color: var(--secondary); color: var(--secondary); background: var(--secondary-container); }
.detail-head .fav, .detail-head .share-btn, .detail-head .fav-form { margin-top: 12px; }
/* Save and Share are the same control shape and belong on one line. The form wrapper is a
   block, so without this it takes the whole row and pushes Share under it. */
.detail-head .fav-form { display: inline-flex; }
.share-url { width: 100%; margin-top: var(--md); padding: 10px 12px; border-radius: 8px;
  border: 1px solid var(--outline-variant); background: var(--surface-container-low);
  font: 400 14px var(--sans); color: var(--on-surface-variant); }
.share-row { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: var(--md); }
/* Copy and the phone's share sheet need a browser that has them. map.js marks the page when it
   does; until then neither is drawn, so no control is shown that would do nothing when pressed. */
.share-js { display: none; }
.can-copy [data-share-copy], .can-share [data-share-more] { display: inline-flex; }
/* Deleting an account is one click behind a disclosure, which is the platform's own confirmation
   and needs no script. Its own class, not .filters: that one hides its summary above 780px, which
   would leave the delete control unreachable on a desktop. */
.confirm { margin-top: var(--md); }
.confirm > summary {
  display: inline-flex; align-items: center; min-height: 44px; cursor: pointer;
  font: 500 14px var(--sans); color: var(--error);
}
.confirm > summary:hover { text-decoration: underline; }
.confirm > p { margin: var(--xs) 0 var(--sm); }
.btn-danger { background: var(--error-container); color: var(--on-error-container); border-color: var(--on-error-container); }
.btn-danger:hover { background: var(--error); color: var(--on-primary); }
/* Same pill family as .fav on the cards, so the header control and the card hearts read as one
   system. The count is never the only signal: the glyph fills and the label stays "Saved". */
.topnav-account {
  display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 8px 14px;
  border: 1px solid var(--outline-variant); border-radius: 999px;
  font: 500 14px var(--sans); color: var(--secondary); text-decoration: none; white-space: nowrap;
  transition: background .2s, border-color .2s;
}
.topnav-account:hover { border-color: var(--secondary); background: var(--surface-container-low); }
.topnav-account[aria-current] { border-color: var(--secondary); background: var(--secondary-container); }
.topnav-count {
  min-width: 20px; padding: 1px 6px; border-radius: 999px; text-align: center;
  background: var(--secondary-container); color: var(--secondary); font: 600 12px/1.4 var(--sans);
  font-variant-numeric: tabular-nums;
}

/* Print. There is deliberately no print button: every browser already has one, and the page URL
   goes in the UA's own print header, so paper still traces back to the record. What the page
   does owe print is the basis behind each figure: a caveat parked in a popover does not print,
   which would leave "€1,778 / m²" on paper as a bare number, the one thing the display rules
   forbid. So the popovers are unfolded inline and the controls that opened them go away. */
@media print {
  .topnav, .site-footer, .skip-link, .btn, .segmented, .gal-thumbs, .gal-nav, .cmp-nav, .map-legend,
  .disclose, .disclose-close, .fav-form, .fav, .share-btn { display: none !important; }
  .disclose-body {
    display: block !important; position: static !important; transform: none; max-width: none;
    box-shadow: none; margin: var(--xs) 0; break-inside: avoid;
  }
  .slab, .result-card, .map-frame, .datalist > div, figure { break-inside: avoid; }
  a { text-decoration: none; color: inherit; }
}

/* Cookie strip. One line at the foot of the window until it is answered, then gone for good.
   It is deliberately small: the question is small, and the page behind it is what the visitor
   came for. Not fixed on narrow screens, where a pinned bar would eat a third of the view. */
.cookie-bar {
  position: sticky; bottom: 0; z-index: 40;
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--xs) var(--sm);
  padding: 10px var(--md);
  background: var(--surface-container-lowest); border-top: 1px solid var(--outline-variant);
  box-shadow: 0 -2px 12px rgb(0 37 49 / .08);
}
.cookie-bar p { flex: 1 1 320px; margin: 0; font: 400 13px/1.5 var(--sans); color: var(--on-surface-variant); }
@media print { .cookie-bar { display: none; } }

/* The village watch form, at the foot of a village's results, and the same checkbox on the
   account page. No new colours and no new radii: it is a .slab with a fieldset in it. */
.watch-form { margin-top: var(--sm); }
.watch-kinds { border: 0; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--xs) var(--sm); }
.watch-kinds legend { padding: 0 0 var(--xs); }
/* 44px minimum, set on the label rather than the box: a 13px checkbox inside a 44px row is a
   13px target unless the label is what takes the press. */
.watch-check { display: flex; align-items: center; gap: var(--xs); min-height: 44px; cursor: pointer;
  font: 400 15px/1.4 var(--sans); color: var(--on-surface); }
.watch-check input { width: 20px; height: 20px; accent-color: var(--secondary); flex: none; }
.watch-row { display: flex; flex-wrap: wrap; gap: var(--xs); margin-top: var(--xs); }
.watch-input { flex: 1 1 260px; min-height: 44px; padding: 0 12px; font: 400 16px/1.4 var(--sans);
  color: var(--on-surface); background: var(--surface-container-lowest);
  border: 1px solid var(--outline-variant); border-radius: var(--r-lg); }
.watch-input:focus-visible { outline: 2px solid var(--secondary); outline-offset: 1px; }
.watch-row .btn { flex: 0 0 auto; min-height: 44px; }
/* Never colour alone: each notice opens with its own word, and the border is the second signal. */
.watch-notice { margin: var(--xs) 0 0; padding: 12px var(--sm); border-radius: var(--r-lg);
  font: 400 15px/1.5 var(--sans); border-left: 3px solid var(--secondary);
  background: var(--surface-container-low); color: var(--on-surface); }
.watch-bad { border-left-color: var(--terracotta, #B96A4A); }
.watch-list { margin: var(--xs) 0 var(--sm) var(--md); }
.digest-form { margin-top: var(--sm); padding-top: var(--sm); border-top: 1px solid var(--outline-variant); }
.digest-form .note { margin-top: var(--xs); }

/* The language link, on the 25 German pages and their English originals and nowhere else. Above
   the heading because the reader who needs it cannot read the heading, and small because the
   other 3,000 pages of this site do not have one and it must not read as navigation. Right
   where a language control is looked for, and it is a link rather than a menu: there are two
   languages and a menu of two is a menu. */
.lang-switch { max-width: var(--shell); margin: 0 auto; padding: var(--xs) var(--margin-desktop) 0;
  text-align: right; font-size: 14px; }
.lang-switch a { color: var(--on-surface-variant); }
.lang-switch a:hover { color: var(--secondary); }
/* The same margin `.shell` takes on a phone. It lives here rather than beside `.shell`'s own
   mobile rule because that rule is 300 lines above this block, and a later rule of equal
   specificity wins whether or not the earlier one is in a media query: written up there, the
   link sat 40px from the edge over a heading sitting at 16. */
@media (max-width: 780px) {
  .lang-switch { padding-left: var(--margin-mobile); padding-right: var(--margin-mobile); }
}
/* The result list carries its own side padding for the split view, where it sits against the
   map with no page margin of its own. Inside the page container it would add to that margin,
   and the cards stood 32px in under a heading standing at 16. */
.shell > .results-list { padding-left: 0; padding-right: 0; }
