/* ==========================================================================
   FlatSpotter — "the drafting board"
   The product reads floorplans, so the page is the surface a floorplan is
   drawn on: cool vellum, a faint measured grid, drafting-ink type, and the
   architect's red pencil reserved for ONE thing — the dimension line that
   carries every square-footage figure. Colour is otherwise rationed to the
   value signal (£/sqft against the area median). Plex Mono sets every
   figure; Plex Sans Condensed sets every label; Plex Sans does the reading.
   ========================================================================== */

/* ------------------------------------------------------------------ fonts */
/* Self-hosted, on purpose. A <link> to fonts.googleapis.com discloses every
   visitor's IP and referring URL to a third party before the first paint —
   the security review flagged it, and a typeface is not worth that. Latin
   subsets only (~90 KB total); IBM Plex is SIL OFL, licence in fonts/. */

@font-face {
    font-family: "IBM Plex Sans";
    src: url("fonts/plex-sans-var.woff2") format("woff2-variations"),
         url("fonts/plex-sans-var.woff2") format("woff2");
    font-weight: 100 700;          /* one variable file covers the range */
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: "IBM Plex Sans Condensed";
    src: url("fonts/plex-cond-500.woff2") format("woff2");
    font-weight: 500;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: "IBM Plex Sans Condensed";
    src: url("fonts/plex-cond-600.woff2") format("woff2");
    font-weight: 600;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: "IBM Plex Mono";
    src: url("fonts/plex-mono-400.woff2") format("woff2");
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: "IBM Plex Mono";
    src: url("fonts/plex-mono-500.woff2") format("woff2");
    font-weight: 500;
    font-style: normal;
    font-display: swap;
}

/* ----------------------------------------------------------------- tokens */

:root {
    /* surfaces — cool drafting vellum, never cream */
    --paper: #F7F8FA;
    --paper-2: #EDF1F6;
    --card: #FFFFFF;
    --grid: #E4E9F0;
    --ink: #12233A;                /* drafting ink: deep blue-black */
    --ink-soft: #5A6B80;
    /* 2.6:1 on white — below AA, so --ink-faint is for DECORATION only
       (placeholders, separators, hairline hovers). Anything a reader has
       to read, however small, uses --ink-soft (5.4:1). */
    --ink-faint: #94A2B4;
    --rule: #C6D0DC;               /* hairline drawing rule */
    --annotate: #C8452D;           /* architect's red pencil — MEASUREMENTS ONLY */
    --value: #0E7C5A;              /* the deal signal */

    /* Legacy aliases: settings.html and older rules still name these. */
    --line: var(--rule);
    --line-strong: var(--rule);

    /* Deal tiers, re-pointed at the palette above. "Expensive" is
       deliberately NOT red — red belongs to the dimension line, and a
       warning colour would overstate what is only a median comparison. */
    --deal-great: var(--value);
    --deal-good: #2F7F63;
    --deal-high: var(--ink-soft);
    --deal-great-bg: #E3F0EA;
    --deal-high-bg: var(--paper-2);

    --danger: #C8452D;
    --ok: var(--value);

    /* type */
    --sans: "IBM Plex Sans", -apple-system, "Segoe UI", system-ui, sans-serif;
    --cond: "IBM Plex Sans Condensed", "IBM Plex Sans", -apple-system, system-ui, sans-serif;
    --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
    /* kept so settings.html's older rules resolve to something sane */
    --serif: var(--sans);

    /* scale */
    --t-eyebrow: 0.6875rem;        /* 11px  condensed caps */
    --t-small: 0.8125rem;          /* 13px */
    --t-body: 0.9375rem;           /* 15px */
    --t-lead: 1.0625rem;           /* 17px */

    /* shape & rhythm — drafting corners are sharp */
    --radius: 3px;
    --radius-lg: 3px;
    --shadow: 0 1px 1px rgba(18, 35, 58, 0.04), 0 6px 18px rgba(18, 35, 58, 0.05);
    --shadow-lift: 0 2px 4px rgba(18, 35, 58, 0.07), 0 16px 34px rgba(18, 35, 58, 0.10);
    --pad: clamp(14px, 2.5vw, 26px);
    --max: 1560px;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html { scrollbar-gutter: stable; }

body {
    font-family: var(--sans);
    color: var(--ink);
    font-size: var(--t-body);
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
    /* The measured grid: minor 8px, major 40px. Faint enough to read as
       paper texture — if it ever reads as a table it is too strong. */
    background-color: var(--paper);
    background-image:
        repeating-linear-gradient(to right, var(--grid) 0 1px, transparent 1px 40px),
        repeating-linear-gradient(to bottom, var(--grid) 0 1px, transparent 1px 40px),
        repeating-linear-gradient(to right,
            color-mix(in srgb, var(--grid) 42%, transparent) 0 1px, transparent 1px 8px),
        repeating-linear-gradient(to bottom,
            color-mix(in srgb, var(--grid) 42%, transparent) 0 1px, transparent 1px 8px);
    background-attachment: fixed;
}

::selection { background: var(--ink); color: var(--paper); }

b, strong { font-weight: 600; }

.sr-only {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

:focus-visible {
    outline: 2px solid var(--ink);
    outline-offset: 2px;
    border-radius: 1px;
}

/* Every figure is set in mono: prices, sizes, counts, distances. Marking
   them with <b> keeps the markup readable and the rule in one place. */
.listing-count b, .pagination-info b, .spec-row b, .topbar-stats b,
.count-median b {
    font-family: var(--mono);
    font-weight: 500;
    font-size: 0.95em;
    letter-spacing: -0.01em;
}

/* Section headers / eyebrows: condensed, uppercase, tracked out. */
.eyebrow, .field-label {
    font-family: var(--cond);
    font-size: var(--t-eyebrow);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.09em;
    color: var(--ink-soft);
}

/* ============================================================ THE SIGNATURE
   The dimension line. Every square footage on this site is drawn as an
   architectural dimension annotation — a hairline in red pencil, a tick at
   each end, the measurement sitting on it:   |———— 782 SQFT ————|
   Pure CSS: the arms are the two pseudo-elements, each painting its own
   end tick plus its half of the rule with background gradients. Used in
   exactly two places, the hero stat and the card, and nowhere else — it is
   the one thing this page is remembered by. */

.dim {
    --dim-tick: 9px;
    display: flex;
    align-items: center;
    color: var(--annotate);
    width: 100%;
}

.dim::before,
.dim::after {
    content: "";
    flex: 1 1 auto;
    min-width: 14px;
    height: var(--dim-tick);
    /* end tick (1px wide, full height) + the rule (1px tall, full width) */
    background:
        linear-gradient(currentColor, currentColor) no-repeat left center / 1px 100%,
        linear-gradient(currentColor, currentColor) no-repeat center / 100% 1px;
}
.dim::after {
    background:
        linear-gradient(currentColor, currentColor) no-repeat right center / 1px 100%,
        linear-gradient(currentColor, currentColor) no-repeat center / 100% 1px;
}

.dim-body {
    display: inline-flex;
    align-items: baseline;
    gap: 0.45em;
    padding: 0 0.7em;
    white-space: nowrap;
}

.dim-fig {
    font-family: var(--mono);
    font-weight: 500;
    letter-spacing: -0.02em;
    font-variant-numeric: lining-nums tabular-nums;
}

/* THE SEPARATOR SQUEEZE, and the two places it must not happen.
   fig() in index.html wraps every comma and decimal point in .nsep so a
   monospace figure can pull them out of their own empty cell — without it
   "14,363" is set as "14 , 363" and reads as two numbers.

   It is OFF by default and switched on per monospace context below,
   because the trick is a fix for one font. In the condensed face the comma
   is already narrow, and -0.2em drags it clean on top of the digit before
   it: "£1,842" came out as "£1̶842", which reads as struck-through text —
   a sibling listing looking like a cancelled price. So a figure only gets
   the squeeze where the type it is set in actually needs it, and a fig()
   dropped into some new proportional-font corner is correct by default. */
.nsep { margin-inline: 0; }

.listing-count b .nsep, .pagination-info b .nsep, .spec-row b .nsep,
.topbar-stats b .nsep, .count-median b .nsep, .dim-fig .nsep,
.ppsf-fig .nsep, .price-pcm .nsep, .reduced-tag .nsep {
    margin-inline: -0.2em;
}

/* ...and off again under a line-through, even in mono. The negative
   margins make the separator's inline box overlap its neighbours', and the
   browser paints the strike per box, so the rule crosses the squeezed
   comma twice — a thickening and a nick, right where the eye is already
   working to read a cancelled figure. Struck text gives the separator its
   cell back: one atom wider, and legible instead of scarred.

   `.struck` is the ONLY place a line-through is drawn (grep it), so the
   decoration and its exemption cannot drift apart — anything that wants
   text struck takes this class rather than declaring text-decoration
   itself. This rule must stay AFTER the mono list above, which it ties
   with on specificity. */
.struck { text-decoration: line-through; }
.struck .nsep { margin-inline: 0; }

.dim-unit {
    font-family: var(--cond);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    font-size: 0.68em;
}

/* hero stat */
.dim--hero {
    --dim-tick: 11px;
    max-width: 27rem;
    margin-top: clamp(13px, 1.8vw, 19px);
}
.dim--hero .dim-fig { font-size: clamp(1.4rem, 2.5vw, 1.75rem); }
.dim--hero .dim-unit { font-size: 0.72rem; }

/* card */
.dim--card {
    --dim-tick: 8px;
    margin-bottom: 2px;
}
.dim--card .dim-fig { font-size: 1.05rem; }
.dim--card .dim-unit { font-size: 0.66rem; }

/* Stated by the agent rather than measured off a plan: same annotation,
   no red pencil — we did not draw this one. */
.dim--stated { color: var(--ink-faint); }

/* No floorplan we could read: the dimension is dashed, the way an unknown
   run is dashed on a drawing. */
.dim--none { color: var(--ink-faint); }
.dim--none::before {
    background:
        linear-gradient(currentColor, currentColor) no-repeat left center / 1px 100%,
        repeating-linear-gradient(to right, currentColor 0 3px, transparent 3px 6px)
            no-repeat center / 100% 1px;
}
.dim--none::after {
    background:
        linear-gradient(currentColor, currentColor) no-repeat right center / 1px 100%,
        repeating-linear-gradient(to right, currentColor 0 3px, transparent 3px 6px)
            no-repeat center / 100% 1px;
}

/* THE one orchestrated moment on the page: the hero's rule draws outward
   from the centre as the page settles. Nothing else moves on load, and
   nothing at all moves on scroll. */
@keyframes dim-draw-left  { from { clip-path: inset(0 0 0 100%); } to { clip-path: inset(0); } }
@keyframes dim-draw-right { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0); } }
@keyframes dim-settle     { from { opacity: 0; } to { opacity: 1; } }

.dim--hero::before { animation: dim-draw-left  400ms cubic-bezier(.22,.7,.2,1) both; }
.dim--hero::after  { animation: dim-draw-right 400ms cubic-bezier(.22,.7,.2,1) both; }
.dim--hero .dim-body { animation: dim-settle 400ms ease both; }

/* --------------------------------------------------------------- top bar */

.topbar {
    position: sticky;
    top: 0;
    z-index: 40;
    background: color-mix(in srgb, var(--paper) 86%, transparent);
    backdrop-filter: blur(10px);
    border-bottom: 1px solid var(--rule);
}

.topbar-inner {
    max-width: var(--max);
    margin: 0 auto;
    padding: 11px var(--pad);
    display: flex;
    align-items: baseline;
    gap: 18px;
}

.wordmark {
    font-family: var(--cond);
    font-size: 1.05rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.16em;
    color: var(--ink);
    text-decoration: none;
    white-space: nowrap;
}

.topbar-note {
    font-family: var(--cond);
    font-size: var(--t-eyebrow);
    text-transform: uppercase;
    letter-spacing: 0.09em;
    color: var(--ink-soft);
    padding-left: 16px;
    border-left: 1px solid var(--rule);
    align-self: center;
}

.topbar-stats {
    display: flex;
    gap: 16px;
    font-size: var(--t-small);
    color: var(--ink-soft);
    flex: 1;
    min-width: 0;
    flex-wrap: wrap;
}

.topbar-actions { display: flex; gap: 8px; margin-left: auto; }

/* Public build: the bar's one action — the aside's Telegram button
   restated at a size that defers to the hero. */
.topbar-cta {
    margin-left: auto;
    align-self: center;
    padding: 5px 12px;
    font-size: 0.75rem;
}

/* --------------------------------------------------------------- buttons */

.btn {
    display: inline-block;
    font-family: var(--cond);
    font-size: 0.82rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    padding: 8px 16px;
    border-radius: var(--radius);
    border: 1px solid transparent;
    cursor: pointer;
    text-decoration: none;
    text-align: center;
    white-space: nowrap;
    transition: background 0.15s, border-color 0.15s, color 0.15s;
}

.btn-primary { background: var(--ink); color: var(--paper); }
.btn-primary:hover { background: #0A1728; }

.btn-quiet {
    background: transparent;
    color: var(--ink-soft);
    border-color: var(--rule);
}
.btn-quiet:hover { border-color: var(--ink-soft); color: var(--ink); }

.btn-lg { padding: 12px 22px; font-size: 0.86rem; }

.btn-disabled { opacity: 0.35; pointer-events: none; }

/* ------------------------------------------------------- search deck/hero */

.deck { padding: 16px 0; }

.deck-inner {
    max-width: var(--max);
    margin: 0 auto;
    padding: 0 var(--pad);
}

/* Public build: the search instrument IS the hero — a sheet pinned to the
   drafting board, with the display line above it. The vertical budget is
   set by the laptop it will actually be read on: at 1440x900 the search
   row and the first row of cards have to be on screen without scrolling,
   because the search box is the product and a masthead nobody scrolled
   past is decoration. Nothing is removed to get there — the display line,
   the sub and the measured-flats rule all still lead — they are simply
   drawn at the size a page with results on it can afford. */
.deck--hero { padding: clamp(22px, 3.2vw, 38px) 0 clamp(8px, 1.4vw, 14px); }

.hero { max-width: 52rem; margin-bottom: clamp(14px, 2vw, 22px); }

.hero-line {
    font-size: clamp(1.95rem, 4vw, 2.7rem);
    font-weight: 600;
    line-height: 1.03;
    letter-spacing: -0.032em;
}

.hero-sub {
    margin-top: 9px;
    font-size: clamp(0.95rem, 1.35vw, 1.06rem);
    line-height: 1.45;
    color: var(--ink-soft);
    max-width: 34rem;
}

/* ----------------------------------------------------------- search form */

.searchform {
    background: var(--card);
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 15px 16px;
}

.searchform-row {
    display: flex;
    align-items: flex-end;
    gap: 18px;
    flex-wrap: wrap;
}

.field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
/* The area box takes the slack: it is the control people reach for first,
   and a wide field is the difference between "type a place" and "pick from
   a list of 554". */
.field--area { flex: 1 1 280px; max-width: 620px; }

.field select,
.field input {
    font-family: var(--sans);
    font-size: 0.9rem;
    color: var(--ink);
    background: var(--card);
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    padding: 7px 10px;
    height: 38px;
    min-width: 0;
}
.field input::placeholder { color: var(--ink-faint); }
.field select:focus, .field input:focus {
    outline: none;
    border-color: var(--ink);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--ink) 9%, transparent);
}
.field select { min-width: 190px; max-width: 300px; }

.range-pair { display: flex; align-items: center; gap: 5px; }
.range-pair input { width: 80px; font-family: var(--mono); font-size: 0.85rem; }
.field--beds .range-pair input { width: 58px; }
.range-dash { color: var(--ink-faint); }

/* hide number spinners; they fight the compact layout */
.range-pair input::-webkit-outer-spin-button,
.range-pair input::-webkit-inner-spin-button { -webkit-appearance: none; }
.range-pair input[type="number"] { -moz-appearance: textfield; appearance: textfield; }

.searchform-actions { display: flex; gap: 8px; align-items: center; margin-left: auto; }
.searchform-actions .btn { height: 38px; line-height: 22px; }

.filters-advanced {
    display: none;
    gap: 22px;
    flex-wrap: wrap;
    margin-top: 14px;
    padding-top: 13px;
    border-top: 1px solid var(--rule);
}
.filters-advanced.show { display: flex; }

.checkbox-label {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: var(--t-small);
    color: var(--ink-soft);
    cursor: pointer;
}
.checkbox-label input { accent-color: var(--ink); width: 15px; height: 15px; }

/* --------------------------------------------------------- area combobox */

.combo { position: relative; }
.combo > input { width: 100%; }

.combo-list {
    position: absolute;
    z-index: 60;
    top: calc(100% + 5px);
    left: 0;
    right: 0;
    max-height: 340px;
    overflow-y: auto;
    list-style: none;
    background: var(--card);
    border: 1px solid var(--ink-faint);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lift);
}
.combo-list[hidden] { display: none; }

.combo-opt {
    display: flex;
    align-items: baseline;
    gap: 10px;
    padding: 8px 12px;
    cursor: pointer;
}
.combo-opt + .combo-opt { border-top: 1px solid var(--paper-2); }
.combo-opt.active, .combo-opt:hover { background: var(--paper-2); }

.combo-name { flex: 1 1 auto; font-size: 0.9rem; min-width: 0; }
.combo-kind {
    font-family: var(--cond);
    font-size: 0.62rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.09em;
    color: var(--ink-soft);
}
.combo-opt.active .combo-kind { color: var(--ink-soft); }
.combo-n {
    font-family: var(--mono);
    font-size: 0.76rem;
    color: var(--ink-soft);
    font-variant-numeric: tabular-nums;
}
.combo-opt.active .combo-n { color: var(--ink-soft); }

.combo-empty {
    padding: 10px 12px;
    font-size: var(--t-small);
    color: var(--ink-soft);
}

/* -------------------------------------------------------------- view bar */

.viewbar {
    max-width: var(--max);
    margin: 0 auto;
    /* Part of the same vertical budget as the hero above: every band
       between the search box and the first card is a band of nothing on
       a 900px-tall laptop. */
    padding: 16px var(--pad) 0;
    display: flex;
    align-items: center;
    gap: 14px;
    flex-wrap: wrap;
}

.viewbar-group {
    display: flex;
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--card);
}

.viewtab {
    font-family: var(--cond);
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ink-soft);
    text-decoration: none;
    padding: 7px 13px;
    border-right: 1px solid var(--rule);
    transition: background 0.15s, color 0.15s;
}
.viewtab:last-child { border-right: none; }
.viewtab:hover { color: var(--ink); background: var(--paper-2); }
.viewtab.active { background: var(--ink); color: var(--paper); }

.chips { display: flex; gap: 8px; flex-wrap: wrap; }

.chip {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    font-family: var(--cond);
    font-size: 0.76rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--ink);
    background: var(--card);
    border: 1px dashed var(--rule);
    border-radius: var(--radius);
    padding: 5px 10px;
    text-decoration: none;
    transition: border-color 0.15s, color 0.15s;
}
.chip:hover { border-color: var(--danger); color: var(--danger); }
.chip-x { font-size: 1rem; line-height: 1; color: var(--ink-faint); }
.chip:hover .chip-x { color: var(--danger); }

/* ------------------------------------------------------------- the sheet */

.sheet {
    max-width: var(--max);
    margin: 0 auto;
    padding: 16px var(--pad) 60px;
}

.listing-head {
    display: flex;
    align-items: baseline;
    gap: 14px;
    flex-wrap: wrap;
    margin-bottom: 14px;
    padding-bottom: 10px;
    border-bottom: 1px solid var(--rule);
}

.listing-title {
    font-family: var(--cond);
    font-size: 1.05rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.09em;
}

.listing-count { font-size: var(--t-small); color: var(--ink-soft); }
.count-median { color: var(--ink-soft); }

.grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(310px, 1fr));
    gap: 20px;
}

/* ------------------------------------------------------------------ card */

.card {
    background: var(--card);
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    overflow: hidden;
    box-shadow: var(--shadow);
    position: relative;
    display: flex;
    flex-direction: column;
    transition: box-shadow 0.18s, border-color 0.18s;
}
.card:hover { box-shadow: var(--shadow-lift); border-color: var(--ink-faint); }

.card.hidden { opacity: 0.55; }
.card.hidden::after {
    content: "hidden";
    position: absolute;
    top: 10px;
    left: 10px;
    font-family: var(--cond);
    font-size: 0.68rem;
    font-weight: 600;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    background: var(--ink);
    color: var(--paper);
    padding: 3px 8px;
    border-radius: var(--radius);
}

/* Photograph dominant, floorplan inset.
   These two images do different jobs and an equal split served neither.
   The floorplan is the PROOF — it is where the sqft on this card was
   measured, and it stays visible and zoomable. But it is mostly white
   space with hairlines in it, so at half the card it read as a blank
   panel while the photograph, the thing that makes someone want the
   flat, was cropped into a letterbox. So: the photo takes the whole
   media area and the plan sits on top of it as a small framed inset,
   the way a plan sits on an estate agent's board. */
.card-images {
    position: relative;
    display: flex;
    height: 204px;
    background: var(--paper-2);
    border-bottom: 1px solid var(--rule);
}

/* The blank underneath. Every card carries it; images simply sit on top,
   so a listing whose photo AND plan both fail upstream degrades to a
   quiet piece of drafting board with a label — never an empty box, and
   never a layout that shifts when an image drops out. */
.card-images::before {
    content: "no image";
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: var(--cond);
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ink-faint);
    background-image:
        repeating-linear-gradient(to right, var(--rule) 0 1px, transparent 1px 22px),
        repeating-linear-gradient(to bottom, var(--rule) 0 1px, transparent 1px 22px);
    opacity: 0.75;
}

.card-images img {
    position: relative;   /* above the blank */
    flex: 1;
    min-width: 0;
    object-fit: cover;
    cursor: zoom-in;
    /* Opaque while loading, so the blank underneath is only ever seen
       once an image has actually FAILED. Without this, every card flashes
       "no image" for as long as its photo takes to arrive. */
    background: var(--paper-2);
}

.card-images img.floorplan {
    position: absolute;
    right: 10px;
    bottom: 10px;
    width: 37%;
    max-width: 134px;
    height: 50%;
    flex: none;
    object-fit: contain;
    background: #fff;
    /* Framed and lifted: a plan is nearly all white, so without an edge
       and a shadow it dissolves into a pale photograph instead of
       reading as a separate object laid on top of one. */
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    box-shadow: 0 2px 10px rgb(0 0 0 / 0.22);
    padding: 3px;
}
.card-images img.floorplan:hover { box-shadow: 0 3px 14px rgb(0 0 0 / 0.32); }

/* Sole survivor. The photo died upstream (or was never scraped) and the
   plan is all this listing has left, so it takes the whole area back —
   an inset floating on a blank drafting board would be a worse card than
   the plan itself. This should now be RARE: a dead photo arms the listing
   for a details re-fetch (db.mark_photo_stale), so seeing many of these
   means healing is not keeping up. */
.card-images img.floorplan:only-of-type {
    position: relative;
    inset: auto;
    width: auto;
    height: auto;
    max-width: none;
    flex: 1;
    border: none;
    border-radius: 0;
    box-shadow: none;
    padding: 4px;
}

.hide-btn {
    position: absolute;
    top: 8px;
    right: 8px;
    width: 30px;
    height: 30px;
    border: none;
    border-radius: 50%;
    background: color-mix(in srgb, var(--ink) 55%, transparent);
    color: #fff;
    font-size: 1.15rem;
    line-height: 1;
    cursor: pointer;
    opacity: 0.65;
    transition: opacity 0.15s, background 0.15s;
    backdrop-filter: blur(2px);
}
.hide-btn:hover { opacity: 1; background: var(--ink); }

/* touch devices get no hover reveal — keep it comfortably tappable */
@media (hover: none) {
    .hide-btn { opacity: 0.85; width: 42px; height: 42px; font-size: 1.4rem; }
}

/* Bottom-LEFT: bottom-right is where the floorplan inset now sits. */
.unhide-btn {
    position: absolute;
    bottom: 8px;
    left: 8px;
    border: none;
    border-radius: var(--radius);
    background: var(--ink);
    color: var(--paper);
    font-size: 0.78rem;
    font-weight: 600;
    padding: 5px 10px;
    cursor: pointer;
}

.card-body {
    padding: 13px 15px 14px;
    display: flex;
    flex-direction: column;
    gap: 7px;
    flex: 1;
}

/* £/sqft — the value question, in mono because it is a figure */
.ppsf-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
}

.ppsf-fig {
    font-family: var(--mono);
    font-size: 1.55rem;
    font-weight: 500;
    letter-spacing: -0.03em;
    font-variant-numeric: lining-nums tabular-nums;
    color: var(--ink);
    line-height: 1.1;
}

.ppsf-fig small {
    font-family: var(--cond);
    font-size: 0.66rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.09em;
    color: var(--ink-soft);
    margin-left: 4px;
}

.ppsf-fig.na { color: var(--ink-faint); }

.deal-great .ppsf-fig { color: var(--deal-great); }
.deal-good .ppsf-fig { color: var(--deal-good); }
.deal-high .ppsf-fig { color: var(--ink); }

.deal-tag {
    font-family: var(--cond);
    font-size: 0.72rem;
    font-weight: 600;
    /* Sentence case, not uppercase. The tag says "49% below London
       median" — a phrase, and uppercase set it shouting in a register
       that also cost it the noun ("49% BELOW LONDON" read as a label and
       had nowhere to put "median"). The tracking that made caps legible
       is dropped with them. */
    letter-spacing: 0.005em;
    padding: 3px 8px;
    border-radius: var(--radius);
    /* The tag now names the area it compares against, and London has
       "Kensington and Chelsea" in it. Wrapping inside the tag costs one
       short line on the few cards that need it; nowrap cost 27px of
       overflow on every card in that borough. */
    white-space: normal;
    text-wrap: balance;
    line-height: 1.25;
    text-align: right;
    max-width: 62%;
}
.deal-great .deal-tag { background: var(--deal-great-bg); color: var(--deal-great); }
.deal-good .deal-tag { background: var(--deal-great-bg); color: var(--deal-good); }
.deal-high .deal-tag { background: var(--deal-high-bg); color: var(--deal-high); }

/* Out of band for its peers. Deliberately the quietest tag on the card and
   drawn as an unfinished note — dashed rule, no fill — because it is not an
   accusation, it is an admission: we have not checked this one. The figure
   beside it loses its colour for the same reason. */
.deal-unchecked .ppsf-fig { color: var(--ink-faint); }
/* A price caveat is not the same admission. "Unchecked" doubts the FIGURE
   — the floorplan may have been read too large — so the figure fades with
   it. "Short let" and "price unclear" doubt only what the figure may be
   COMPARED with: £5.19/sqft is still £1,712 over 330 sqft and still true
   of this listing. It loses its colour, not its legibility. */
.deal-caveat .ppsf-fig { color: var(--ink-soft); }
.deal-unchecked .deal-tag,
.deal-caveat--unclear .deal-tag {
    color: var(--ink-faint);
    background: transparent;
    border: 1px dashed var(--rule);
    padding: 2px 7px;
}
/* ...and "short let" is neither doubt nor admission — it is a fact we
   read off the listing, so it is set as a plain label. The dashed rule is
   reserved for the two tags that mean "we could not check this". */
.deal-caveat--short .deal-tag {
    color: var(--ink-soft);
    background: var(--paper-2);
}
.price-row { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }

.price-pcm {
    font-family: var(--mono);
    font-size: 1rem;
    font-weight: 500;
    color: var(--ink-soft);
    font-variant-numeric: tabular-nums;
}
.price-pcm small {
    font-family: var(--cond);
    font-size: 0.66rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.09em;
    color: var(--ink-soft);
    margin-left: 3px;
}

.reduced-tag {
    font-family: var(--mono);
    font-size: 0.78rem;
    color: var(--value);
    text-decoration-color: color-mix(in srgb, var(--value) 55%, transparent);
}

.negotiable-tag {
    font-family: var(--cond);
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--ink-soft);
    background: var(--paper-2);
    border-radius: var(--radius);
    padding: 2px 8px;
    white-space: nowrap;
}

/* The same home, posted more than once. No longer a pill announcing a
   count — a lead-in and one link per sibling, because the only useful
   thing to do with "there is another listing" is open it. */
.group-row {
    font-family: var(--cond);
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    /* --ink-soft, not --ink-faint: this is a sentence a reader has to
       read, and --ink-faint is documented decoration-only (2.6:1). */
    color: var(--ink-soft);
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px 7px;
}

.group-sib {
    color: var(--ink-soft);
    background: var(--paper-2);
    border-radius: var(--radius);
    padding: 3px 8px;
    text-decoration: none;
    white-space: nowrap;
    transition: color 0.12s, background 0.12s;
}
.group-sib:hover {
    color: var(--ink);
    background: var(--rule);
}

/* Address as a drawing label: condensed, uppercase, tracked. */
.addr-row {
    font-family: var(--cond);
    font-size: 0.86rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.055em;
    color: var(--ink);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.spec-row { font-size: var(--t-small); color: var(--ink-soft); }
.spec-sep { color: var(--rule); margin: 0 3px; }
.spec-area { color: var(--ink-soft); }

.meta-row {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: auto;
    padding-top: 8px;
    border-top: 1px solid var(--paper-2);
    flex-wrap: wrap;
}

.meta-chip {
    font-family: var(--cond);
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--ink-soft);
    background: var(--paper-2);
    border-radius: var(--radius);
    padding: 2px 9px;
}

.meta-time {
    font-family: var(--cond);
    font-size: 0.73rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--ink-soft);
}

.card-link {
    margin-left: auto;
    font-family: var(--cond);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ink-soft);
    text-decoration: none;
    border-bottom: 1px solid var(--rule);
    transition: color 0.15s, border-color 0.15s;
}
.card-link:hover { color: var(--ink); border-color: var(--ink); }

/* -------------------------------------------------------------- map view */

.map-canvas {
    height: calc(100vh - 320px);
    min-height: 480px;
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    background: var(--paper-2);
}

.map-pop { font-family: var(--sans); min-width: 180px; }
.map-pop img { width: 100%; height: 110px; object-fit: cover; border-radius: 2px; margin-bottom: 6px; }
.map-pop-ppsf { font-family: var(--mono); font-size: 1.25rem; font-weight: 500; line-height: 1.1; }
.map-pop-ppsf small { font-family: var(--cond); font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-soft); }
.map-pop-price { font-family: var(--mono); font-size: 0.8rem; color: var(--ink-soft); margin: 3px 0; }
.map-pop-addr { font-family: var(--cond); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink); margin-bottom: 4px; }
.map-pop a { font-family: var(--cond); font-size: 0.78rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-soft); }

/* ----------------------------------------------------------- empty state */

.empty-state {
    grid-column: 1 / -1;
    text-align: center;
    padding: 80px 20px;
    border: 1px dashed var(--rule);
    border-radius: var(--radius);
    background: color-mix(in srgb, var(--card) 60%, transparent);
}

.empty-title { font-size: 1.6rem; font-weight: 600; letter-spacing: -0.02em; margin-bottom: 8px; }
.empty-sub { color: var(--ink-soft); margin-bottom: 22px; }

/* ------------------------------------------------------------ pagination */

.pagination {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 16px;
    margin-top: 36px;
}

.pagination-info {
    font-family: var(--cond);
    font-size: var(--t-small);
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--ink-soft);
}

/* --------------------------------------------------------- how it works */

.explain {
    margin-top: clamp(56px, 8vw, 92px);
    padding-top: 16px;
    border-top: 1px solid var(--ink);
}

.steps {
    list-style: none;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
    gap: clamp(20px, 3vw, 40px);
    margin-top: 22px;
}

.step { padding-top: 14px; border-top: 1px solid var(--rule); }

.step-n {
    font-family: var(--mono);
    font-size: 0.78rem;
    font-weight: 500;
    letter-spacing: 0.12em;
    color: var(--ink-soft);
}

.step-h { font-size: 1.05rem; font-weight: 600; margin: 8px 0 6px; letter-spacing: -0.01em; }
.step-p { font-size: var(--t-small); line-height: 1.6; color: var(--ink-soft); max-width: 32ch; }

/* -------------------------------------------------------------- telegram */

.cta {
    margin-top: clamp(36px, 5vw, 56px);
    background: var(--card);
    border: 1px solid var(--rule);
    border-left: 3px solid var(--ink);
    border-radius: var(--radius);
    padding: 22px clamp(18px, 3vw, 28px);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 24px;
    flex-wrap: wrap;
}

.cta-p {
    margin-top: 7px;
    font-size: var(--t-body);
    color: var(--ink-soft);
    max-width: 46ch;
}

/* -------------------------------------------------------------- lightbox */

.lightbox {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: none;
    align-items: center;
    justify-content: center;
    background: color-mix(in srgb, var(--ink) 90%, transparent);
    cursor: zoom-out;
    padding: 3vh 3vw;
}
.lightbox.show { display: flex; }

.lightbox img {
    max-width: 100%;
    max-height: 100%;
    border-radius: var(--radius);
    background: #fff;
    box-shadow: var(--shadow-lift);
}

.lightbox-close {
    position: absolute;
    top: 16px;
    right: 20px;
    width: 40px;
    height: 40px;
    border: none;
    border-radius: 50%;
    background: var(--paper);
    color: var(--ink);
    font-size: 1.5rem;
    line-height: 1;
    cursor: pointer;
}

/* --------------------------------------------------------- settings page */
/* Private build only. `.section` is the settings container — the listing
   page uses `.sheet`, so the two can no longer collide. */

.section {
    max-width: var(--max);
    margin: 0 auto;
    padding: 18px var(--pad) 60px;
}

.section-title {
    font-family: var(--cond);
    font-size: 1rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.09em;
    margin: 30px 0 14px;
}

.flash-messages { max-width: var(--max); margin: 14px auto 0; padding: 0 var(--pad); }

.flash {
    padding: 10px 14px;
    border-radius: var(--radius);
    font-size: 0.88rem;
    font-weight: 500;
    margin-bottom: 8px;
    border: 1px solid;
}
.flash-success { background: var(--deal-great-bg); color: var(--deal-great); border-color: color-mix(in srgb, var(--deal-great) 30%, transparent); }
.flash-error { background: #F7E4E0; color: var(--danger); border-color: color-mix(in srgb, var(--danger) 30%, transparent); }

.search-cards {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
    gap: 20px;
}

.search-card, .add-search-card, .help-card {
    background: var(--card);
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 18px;
}

.search-card-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 12px;
}

.search-card-header h3 { font-size: 1.1rem; font-weight: 600; letter-spacing: -0.01em; }

.btn-delete {
    background: none;
    border: none;
    color: var(--ink-faint);
    font-size: 0.8rem;
    font-weight: 600;
    cursor: pointer;
    padding: 4px 6px;
    border-radius: var(--radius);
}
.btn-delete:hover { color: var(--danger); background: #F7E4E0; }

.search-form { display: flex; flex-direction: column; gap: 12px; }

.form-row-group { display: flex; gap: 12px; }
.form-row-group .form-row { flex: 1; }

.form-row { display: flex; flex-direction: column; gap: 4px; }

.form-row label {
    font-family: var(--cond);
    font-size: var(--t-eyebrow);
    font-weight: 600;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--ink-faint);
}

.form-row input, .form-row select {
    font-family: var(--sans);
    font-size: 0.88rem;
    color: var(--ink);
    background: var(--card);
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    padding: 7px 9px;
}
.form-row input:focus, .form-row select:focus { outline: none; border-color: var(--ink); }
.form-row input[readonly] { background: var(--paper-2); color: var(--ink-soft); }

.search-status {
    display: flex;
    gap: 14px;
    flex-wrap: wrap;
    font-size: var(--t-small);
    color: var(--ink-faint);
    border-top: 1px solid var(--rule);
    padding-top: 10px;
}

.status-pending { color: var(--danger); font-weight: 600; }

.status-link {
    color: var(--ink-soft);
    font-weight: 600;
    text-decoration: none;
    border-bottom: 1px solid var(--rule);
}
.status-link:hover { color: var(--ink); border-color: var(--ink); }

.btn-save { align-self: flex-start; }
.btn-save:disabled { opacity: 0.4; cursor: default; }
.btn-save:not(:disabled) { background: var(--ink); color: var(--paper); }

.lookup-section { margin-bottom: 4px; }
.lookup-input-row { display: flex; gap: 8px; }
.lookup-input-row input { flex: 1; }

.lookup-status { font-size: 0.85rem; margin-top: 8px; }
.lookup-status.loading { color: var(--ink-faint); }
.lookup-status.error { color: var(--danger); font-weight: 500; }

.radio-option {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 0.88rem;
    padding: 6px 4px;
    cursor: pointer;
}
.radio-option input { accent-color: var(--ink); }

.divider { border: none; border-top: 1px solid var(--rule); margin: 16px 0; }

.help-text { font-size: 0.78rem; color: var(--ink-faint); }
.help-note { margin-top: 14px; font-size: 0.85rem; color: var(--ink-soft); }
.help-card ol { padding-left: 20px; display: flex; flex-direction: column; gap: 6px; }
.help-card a { color: var(--ink); }
.help-card code {
    font-family: var(--mono);
    background: var(--paper-2);
    border-radius: 2px;
    padding: 1px 5px;
    font-size: 0.85em;
}

.no-searches { color: var(--ink-faint); font-style: italic; }

.inline-form { display: inline; }

/* ------------------------------------------------------------ responsive */

@media (max-width: 900px) {
    .searchform-actions { margin-left: 0; }
}

@media (max-width: 760px) {
    .topbar-inner { flex-wrap: wrap; gap: 6px 16px; }
    /* On a phone the bar stays one clean row — wordmark left, CTA right —
       and the tagline takes its own full line beneath. */
    .topbar-note { padding-left: 0; border-left: none; order: 3; flex-basis: 100%; }
    .topbar-stats { order: 3; flex-basis: 100%; }

    /* Below the desktop clamp's own floor, so the phone never gets MORE
       masthead than the laptop does. */
    .deck--hero { padding-top: 20px; }
    .searchform-row { gap: 14px; }
    .field--area { flex-basis: 100%; max-width: none; }
    .range-pair input { width: 72px; }

    .viewbar { gap: 10px; padding-top: 18px; }

    .grid { grid-template-columns: 1fr; }
    /* One card per row, so the media area is much wider than on desktop —
       give the photograph the height to match, and cap the inset in
       absolute terms so it does not scale into a second photograph. */
    .card-images { height: 240px; }
    .card-images img.floorplan { width: 34%; max-width: 124px; height: 48%; }

    .form-row-group { flex-direction: column; gap: 12px; }
}

/* 360px floor: nothing may overflow the viewport at the narrowest phone. */
@media (max-width: 420px) {
    .searchform { padding: 13px; }
    .searchform-row { gap: 12px; }
    .field { flex: 1 1 100%; }
    .range-pair { width: 100%; }
    .range-pair input { flex: 1 1 0; width: auto; }
    .searchform-actions { width: 100%; }
    .searchform-actions .btn { flex: 1 1 auto; padding: 8px 10px; }
    .hero-line { font-size: 2rem; }
    .viewbar-group { flex: 1 1 auto; }
    .viewtab { flex: 1 1 auto; text-align: center; padding: 7px 9px; }
    .cta .btn { width: 100%; }
}

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after { transition: none !important; animation: none !important; }
}

/* The boundary and place-name licences ask to be credited; small, quiet,
   and at the end, where a credit belongs — but legible, not hidden. */
.attribution {
    max-width: var(--max);
    margin: 0 auto;
    padding: 18px var(--pad) 40px;
    border-top: 1px solid var(--rule);
}
.attribution p {
    margin: 0;
    font-size: var(--t-small);
    line-height: 1.6;
    color: var(--ink-soft);
}


/* The station distance control. Sits under the area box rather than
   beside it: it only exists for one kind of answer, and giving it a
   column of its own would leave a permanent gap in the filter row for
   every search that is not a station. */
.field-radius { margin-top: 6px; }
.field-radius[hidden] { display: none; }
/* Sized to its own content, not to the area box above it: "within 0.5
   mi" in a 300px control reads as a field somebody forgot to fill in. */
.field-radius select { width: auto; min-width: 140px; }

/* The kind badge already exists for boroughs and postcodes; a station
   gets a stronger one because it is the distinction that actually
   misleads — "Clapham" the district and "Clapham High Street" the
   platform are different searches with nearly the same name. */
.combo-opt .combo-kind { white-space: nowrap; }

/* Selected areas, acknowledged inside the Area field itself: after each
   add the page reloads, and a chip on the viewbar's far right was
   invisible from the box (adding areas on a laptop read as a bug). */
.area-chips { margin-bottom: 6px; gap: 6px; }
