/*
 * tarjousui/static/app.css — one layout, three device sizes.
 *
 * Mobile-first, and single-column up to 1100px: the estimator uses the same
 * screen on a phone at the kerb and on a laptop at the office, and two different
 * layouts would mean two different muscle memories.
 *
 * Above 1100px it becomes the three columns of Marko's 2026-08-28 leiska —
 * tiedot | kuva | "Suunnittele töitä" — because that is the screen he specified
 * and reviews on, and on a 1440px display the single column wasted two thirds of
 * the width while the work panel sat below the fold. The phone order is unchanged
 * and is what the `order:` rules below encode: photo, thumbnails, work panel,
 * rows. `display: contents` on the columns is what lets one DOM structure produce
 * both without duplicating markup.
 *
 * Sizing rules that are load-bearing rather than cosmetic:
 *  - every tappable control is >= 44px, the smallest reliable touch target;
 *  - inputs are 16px, because iOS Safari zooms the page on focus below that;
 *  - the totals bar and sheets respect env(safe-area-inset-*) so nothing hides
 *    under the home indicator;
 *  - the photo stage uses dvh, not vh, so the mobile URL bar collapsing does not
 *    change the drawing surface mid-drag.
 */

:root {
  color-scheme: light dark;

  --bg: #f6f7f9;
  --surface: #ffffff;
  --surface-2: #eef1f5;
  --text: #10151c;
  --muted: #5c6672;
  --border: #d7dce3;
  --accent: #1d4ed8;
  --accent-text: #ffffff;
  --danger: #b91c1c;
  --ok: #15803d;
  --warn: #b45309;

  /* TT / työtapa — the box colour IS the work method (PLAN.md §3). */
  --tt-koysityo: #e11d48;
  --tt-nostintyo: #2563eb;
  --tt-alatyo: #16a34a;
  --tt-sisatyo: #d97706;
  --tt-none: #64748b;

  /* Kohdelistan minikartta: paperi, kadut, puisto, nuppineula. Ei kuvatiedostoa
     riviä kohti — koko kartta on gradientteja ja yksi käännetty neliö. */
  --map-paper: #e9eee7;
  --map-road: #ffffff;
  --map-park: #c9e0c2;
  --map-pin: #e04a3f;
  --map-route: #3f7ff0;

  --radius: 12px;
  --tap: 44px;
  --safe-b: env(safe-area-inset-bottom, 0px);
  /* Top bar + totals bar + gaps — what a sticky side column must not occupy. */
  --chrome-h: 150px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0d1117;
    --surface: #161b22;
    --surface-2: #1f262e;
    --text: #e6edf3;
    --muted: #9aa4b2;
    --border: #2d3742;
    --accent: #3b82f6;
    --danger: #f87171;
    --ok: #4ade80;
    --warn: #fbbf24;

    --map-paper: #1b232b;
    --map-road: #273340;
    --map-park: #22362b;
    --map-pin: #f2705f;
    --map-route: #5b9bff;
  }
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font: 16px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-text-size-adjust: 100%;
  overscroll-behavior-y: none;
}

.view { display: flex; flex-direction: column; min-height: 100dvh; }
.view[hidden] { display: none; }
.muted { color: var(--muted); }
.small { font-size: 13px; }
.pad { padding: 16px; }
.error { color: var(--danger); }

button, input, select, textarea {
  font: inherit;
  color: inherit;
  font-size: 16px; /* below this iOS zooms on focus */
}

button {
  min-height: var(--tap);
  padding: 0 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
  touch-action: manipulation;
}
button:active { transform: translateY(1px); }
button.primary { background: var(--accent); color: var(--accent-text); border-color: transparent; font-weight: 600; }
button.danger { background: transparent; color: var(--danger); border-color: currentColor; }
button.ghost { background: transparent; border-color: transparent; }
button.big { min-height: 52px; width: 100%; font-size: 17px; }
button.icon { min-width: var(--tap); padding: 0; font-size: 22px; line-height: 1; }
button.lang-code { min-width: var(--tap); padding: 0 10px; font-size: 14px; font-weight: 600; letter-spacing: .04em; }
button:disabled { opacity: .5; cursor: not-allowed; }

input[type="text"], input[type="password"], input[type="search"], input[type="number"] {
  width: 100%;
  min-height: var(--tap);
  padding: 0 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
label { display: block; margin: 14px 0 6px; font-size: 13px; font-weight: 600; color: var(--muted); }

/* ── Kirjautuminen ─────────────────────────────────────────────────────── */
#view-login { align-items: center; justify-content: center; padding: 24px; }
.login-card {
  width: 100%; max-width: 380px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 24px;
}
.login-card h1 { margin: 0 0 4px; font-size: 22px; }
.login-card button { margin-top: 20px; }
/* "Kirjaudutaan…" with a ring turning beside it while the sign-in is checked. */
.login-card button[aria-busy="true"]::before {
  content: ''; display: inline-block; width: .9em; height: .9em; margin-right: .5em;
  vertical-align: -.1em;
  border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%;
  animation: kirjautuu .7s linear infinite;
}
@keyframes kirjautuu { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .login-card button[aria-busy="true"]::before { animation: none; }
}

/* ── Palkki ────────────────────────────────────────────────────────────── */
.bar {
  position: sticky; top: 0; z-index: 20;
  display: flex; gap: 8px; align-items: center;
  padding: 10px 12px;
  padding-top: calc(10px + env(safe-area-inset-top, 0px));
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}
.kohde-bar .titles { flex: 1; min-width: 0; display: flex; flex-direction: column; line-height: 1.2; }
.kohde-bar .titles strong { font-size: 17px; }
.kohde-bar .titles span { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The kohde header's line under the address carries the property id since
   2026-10-07, and an id cut to "091-010-06…" is no id: it wraps instead. */
#k-sub { white-space: normal; overflow-wrap: anywhere; }
/* The kohde number and the offer number, and the two map links: above the address on
   a phone, beside it from 700px (below), so the header is no taller there and the
   sticky columns' height (--chrome-h) still holds. */
.kohde-bar .k-otsikkorivi { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 8px; margin-bottom: 2px; }
.kohde-bar .k-otsikkorivi .k-numero { font-size: 16px; }
@media (min-width: 700px) {
  #view-kohde .kohde-bar .titles {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
      "numerot osoite"
      "numerot ala";
    column-gap: 14px; align-items: center;
  }
  #view-kohde .kohde-bar .k-otsikkorivi { grid-area: numerot; margin-bottom: 0; }
  #view-kohde #k-address { grid-area: osoite; }
  #view-kohde #k-sub { grid-area: ala; }
  /* Anything a feature's header decorator (SKT.otsikko) adds goes under the address,
     never under the numbers. */
  #view-kohde .kohde-bar .titles > :not(.k-otsikkorivi) { grid-column: 2; }
}
/* The header's spans are cut with an ellipsis by the rule above; a number's label
   keeps the card's size and is never cut. */
.kohde-bar .k-otsikkorivi .k-numero .lab { font-size: 10px; overflow: visible; }
#back { font-size: 30px; }

/* ── Suodattimet ───────────────────────────────────────────────────────── */
.chips {
  display: flex; gap: 8px; padding: 10px 12px;
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  background: var(--bg);
}
.chip { min-height: 38px; padding: 0 14px; border-radius: 999px; white-space: nowrap; font-size: 14px; }
.chip.active { background: var(--accent); color: var(--accent-text); border-color: transparent; }

/* ── Rivinäkymän rajaukset (P1) ───────────────────────────────────────────
   Postinumero / katu / työviikko / isännöitsijä. A second row under the status
   chips rather than more chips in the same row: a chip is one value out of
   five, and these are one value out of fifty-nine. The row scrolls sideways on
   a phone exactly as the chips above it do. */
.suodattimet {
  display: flex; gap: 8px; align-items: center;
  padding: 0 12px 10px;
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  background: var(--bg);
}
/* The pill is the wrapper now; the text field inside it is the control. See
   app.js::luoSuodatin for why these stopped being <select>s. */
.suodatin {
  position: relative;
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 2px;
  min-height: 36px;
  /* Long enough to read "Kiinteistö Tahkola (14)", short enough that two of
     them fit side by side on a 390px screen. There are five now, so the row
     does scroll on a phone — which is what the sideways scroll above is for;
     what this width protects is that the first two are readable without it. */
  max-width: 46vw;
  padding: 0 8px;
  font-size: 14px;
  border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface); color: inherit;
}
.suodatin:focus-within, .suodatin.auki {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}
/* A filter that is actually narrowing the list reads as filled, so the row
   answers "what is cutting this list down" without being read word by word. */
.suodatin.rajattu {
  background: color-mix(in srgb, var(--accent) 10%, var(--surface));
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
}
/* Two classes deep on purpose: the `input[type="text"], …` rule above is an
   attribute selector and outweighs a single class, so a one-class version of
   this loses and the field keeps its own white box and border inside the pill —
   a box drawn inside a box, which is what the row looked like at first. */
.suodatin .suodatin-kentta {
  flex: 1 1 auto; width: auto; min-width: 0;
  min-height: 34px;
  border: 0; padding: 0; margin: 0;
  background: none; color: inherit; font: inherit;
  text-overflow: ellipsis;
}
.suodatin .suodatin-kentta:focus { outline: none; }
.suodatin .suodatin-kentta::placeholder { color: var(--muted); opacity: 1; }
.suodatin .suodatin-kentta:disabled { color: var(--muted); }
.suodatin:has(.suodatin-kentta:disabled) { opacity: .45; }
.suodatin.rajattu .suodatin-kentta { font-weight: 600; }
/* The pill is the control's height; the field inside it must not add its own. */
.suodatin .suodatin-kentta { min-height: 0; height: 34px; }

/* The three icons. Drawn as SVG by app.js::kuvake — the ▾ and ✕ characters fall
   back to a font whose glyphs are small and sit off the baseline. */
.suodatin-luuppi, .suodatin-nuoli {
  flex: 0 0 auto; display: flex; align-items: center;
  color: var(--muted);
}
.suodatin-luuppi { margin-right: 2px; }
.suodatin-nuoli { cursor: pointer; transition: transform .15s ease; }
.suodatin.auki .suodatin-nuoli { transform: rotate(180deg); }
.suodatin.auki .suodatin-luuppi, .suodatin.rajattu .suodatin-luuppi { color: var(--accent); }
.suodatin-tyhjenna {
  position: relative;
  flex: 0 0 auto; display: flex; align-items: center; justify-content: center;
  /* Overrides `button { min-height: var(--tap) }`, which would otherwise pull a
     22px circle into a 44px-tall ellipse. The tap target is not given up — it
     moves to the ::before below, which is invisible and full size. */
  width: 22px; height: 22px; min-height: 0; padding: 0; margin-right: 2px;
  border: 0; border-radius: 50%;
  background: color-mix(in srgb, var(--text) 8%, transparent);
  color: var(--muted); cursor: pointer;
}
/* A finger needs --tap; the eye wants 22px. This is the difference, spread
   around the circle and catching the taps that miss it. */
.suodatin-tyhjenna::before {
  content: ''; position: absolute;
  top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: var(--tap); height: var(--tap);
}
/* `hidden` sets display:none in the UA sheet, which the display:flex above
   beats — the button would show on every filter, including the empty ones. */
.suodatin-tyhjenna[hidden] { display: none; }
.suodatin-tyhjenna:hover { background: color-mix(in srgb, var(--text) 16%, transparent); }
.suodatin-tyhjenna:active { transform: none; }
.suodatin-tyhjenna:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
@media (prefers-reduced-motion: reduce) {
  .suodatin-nuoli { transition: none; }
}

/* position: fixed, not absolute — .suodattimet scrolls sideways, and a scroll
   container clips absolutely positioned children however high their z-index.
   app.js writes left/top/width from the pill's rect when it opens, and closes
   the list on any scroll rather than trying to follow it. */
.suodatin-lista {
  position: fixed; z-index: 40;
  margin: 4px 0 0; padding: 4px;
  max-height: min(50vh, 320px); overflow-y: auto; -webkit-overflow-scrolling: touch;
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--border); border-radius: 12px;
  box-shadow: 0 8px 24px color-mix(in srgb, var(--text) 18%, transparent);
}
.suodatin-rivi {
  display: flex; align-items: baseline; gap: 10px;
  padding: 9px 10px; border-radius: 8px;
  font-size: 14px; line-height: 1.3;
  cursor: pointer;
}
.suodatin-nimi { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
/* The part that answered the query — app.js::piirraLista puts it in a <b>.
   Weight, not a highlighter: a name can match in three places, and three
   coloured blocks in one line is harder to read than the line was. */
.suodatin-nimi b { font-weight: 700; }
/* The counts line up as a column, which is the point of moving them out of the
   name. Tabular figures so 9 and 140 keep the same right edge. */
.suodatin-maara {
  flex: 0 0 auto;
  color: var(--muted); font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.suodatin-rivi.kohdalla { background: color-mix(in srgb, var(--accent) 16%, transparent); }
.suodatin-rivi.valittu { color: var(--accent); }
.suodatin-rivi.valittu .suodatin-nimi { font-weight: 600; }
/* The tick box of a multi-select row (2026-10-02). Drawn here rather than as an
   SVG per row — the street filter has thousands of rows. The tick is the two
   borders of a rotated box. */
.suodatin-ruutu {
  position: relative; flex: 0 0 auto; align-self: center;
  width: 16px; height: 16px;
  border: 1.5px solid var(--muted); border-radius: 4px;
}
.suodatin-rivi.valittu .suodatin-ruutu { background: var(--accent); border-color: var(--accent); }
.suodatin-rivi.valittu .suodatin-ruutu::after {
  content: ''; position: absolute; left: 4.5px; top: 1px;
  width: 4px; height: 8px;
  border: solid var(--surface); border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
/* ── Rakennusvuosi (app.js::luoVuosiSuodatin) ──────────────────────────────
   The same pill as the five filters; its control is a button rather than a
   search field — a range has nothing to type a name into. Three classes deep to
   beat both `button {}` and the field rule above. */
.suodatin button.suodatin-kentta {
  height: 34px; min-height: 0; padding: 0; margin: 0;
  border: 0; border-radius: 0; background: none;
  font: inherit; color: inherit; text-align: left;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.suodatin button.suodatin-kentta:active { transform: none; }
.suodatin button.suodatin-kentta.paikka { color: var(--muted); }
/* Taller than a filter's list: the two fields and every decade fit at once on a
   phone, so a tapped decade never scrolls the fields out of sight. */
.vuosi-paneeli { padding: 10px; max-height: min(80vh, 520px); }
.vuosi-kentat { display: flex; align-items: flex-end; gap: 8px; margin-bottom: 10px; }
.vuosi-kentta { display: flex; flex-direction: column; gap: 3px; flex: 1 1 0; min-width: 0; }
.vuosi-kentta .lab { font-size: 11px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
.vuosi-kentta input { width: 100%; min-height: 38px; font-variant-numeric: tabular-nums; }
.vuosi-viiva { padding-bottom: 9px; color: var(--muted); }
/* The decades, two or three to a row, each with its count like a filter row. */
.vuosi-lohkot { display: grid; grid-template-columns: repeat(auto-fill, minmax(112px, 1fr)); gap: 4px; }
.vuosi-lohko {
  display: flex; align-items: baseline; justify-content: space-between; gap: 6px;
  min-height: 36px; padding: 0 10px;
  font-size: 13px; text-align: left;
  border-radius: 8px;
}
.vuosi-lohko:active { transform: none; }
.vuosi-lohko.valittu {
  color: var(--accent); font-weight: 600;
  background: color-mix(in srgb, var(--accent) 12%, var(--surface));
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
}
.vuosi-tuntematon { margin: 8px 2px 0; font-size: 12px; color: var(--muted); }

/* ── Järjestys (app.js::luoJarjestys) ──────────────────────────────────────
   A pill like the filters: the icon, the column (a native select — a phone's
   own picker suits fifteen fixed choices), and the direction as a button. */
.jarjestys {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 4px;
  min-height: 36px; padding: 0 4px 0 8px;
  font-size: 14px;
  border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface);
}
.jarjestys.rajattu {
  background: color-mix(in srgb, var(--accent) 10%, var(--surface));
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
}
.jarjestys .suodatin-luuppi { color: var(--muted); }
.jarjestys.rajattu .suodatin-luuppi { color: var(--accent); }
.jarjestys-valinta {
  min-height: 34px; max-width: 46vw; padding: 0 2px;
  border: 0; background: none; color: inherit; font: inherit;
}
.jarjestys-valinta:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-radius: 6px; }
button.jarjestys-suunta {
  min-height: 28px; padding: 0 10px;
  font-size: 12px; font-weight: 600; white-space: nowrap;
  border-radius: 999px; color: var(--accent);
}

/* ── Novan otsikkotiedot (app.js::novaRivit) ───────────────────────────────
   Under the card's own two lines, set apart by a hairline: these are Nova's
   words for the offer, not this tool's merged view of the building. */
.k-nova {
  display: grid; gap: 4px;
  margin-top: 2px; padding-top: 5px;
  border-top: 1px dashed var(--border);
}
body.nova-piilossa .k-nova { display: none; }
.k-nova .badge { align-self: center; }
/* One party to a line at every width: three long blocks side by side wrap into
   each other until nobody can tell whose address is whose. Two classes, so the
   wide-screen `.k-line` row rule below does not win. */
.k-nova .k-nova-osapuolet, .nova-otsikko .k-nova-osapuolet { flex-direction: column; }
.nova-viite { margin-left: 8px; }
.nova-viite .lab { margin-right: 5px; }
/* The kohde page's copy of the block: every number shown, as on the rest of
   that page. */
.nova-otsikko { display: grid; gap: 6px; padding: 10px 14px 12px; border-top: 1px solid var(--border); }
.nova-otsikko h3 { margin: 0; font-size: 12px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
.nova-otsikko .k-line .numero { display: inline; }
.nova-viesti summary { cursor: pointer; font-size: 13px; color: var(--muted); }
.nova-viesti p { margin: 6px 0 0; font-size: 13px; line-height: 1.5; white-space: pre-wrap; }

/* The facts sheet's suggestions (app.js::naytaEhdotukset) — the filter rows'
   look, in the sheet's flow rather than floating, so the keyboard cannot hide
   them on a phone. */
.facts-ehdotukset {
  margin: 4px 0 0; padding: 4px; list-style: none;
  max-height: 220px; overflow-y: auto; -webkit-overflow-scrolling: touch;
  border: 1px solid var(--border); border-radius: 12px;
  background: var(--surface);
}
.facts-ehdotukset[hidden] { display: none; }
/* Under the values that were chosen when the list opened, which come first. */
.suodatin-rivi.erotin { margin-bottom: 5px; box-shadow: 0 5px 0 -4px var(--border); }
@media (hover: hover) {
  .suodatin-rivi:hover { background: color-mix(in srgb, var(--text) 7%, transparent); }
}
.suodatin-tyhja { padding: 9px 10px; font-size: 14px; color: var(--muted); }
/* Only shown while something is actually narrowed — see renderSuodattimet. */
#f-clear { flex: 0 0 auto; min-height: 36px; padding: 0 12px; font-size: 13px; color: var(--muted); }
/* The numbers switch and the photo-size pair, at the end of the row. A pill
   like the chips above it rather than a bare link: these are the controls that
   change what every card shows, and they have to be findable without reading
   the row twice. */
.suodatin-nappi {
  flex: 0 0 auto;
  min-height: 36px; padding: 0 14px;
  font-size: 13px; font-weight: 600;
  border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface); color: var(--accent);
}
.suodatin-nappi[aria-pressed="true"] { background: color-mix(in srgb, var(--accent) 12%, transparent); }

/* Kuvien koko — two glyph buttons after the numbers switch. Square rather than
   pill-wide so the pair reads as one control with two directions, at the same
   36px height as everything else in the row. Disabled at each end of the ladder
   rather than hidden: a control that vanishes is one the thumb hunts for. */
.kuvakoko { flex: 0 0 auto; display: flex; gap: 4px; align-items: center; }
.kuvakoko-nappi { min-width: 36px; padding: 0; font-size: 17px; line-height: 1; }
.kuvakoko-nappi:disabled { opacity: .35; color: var(--muted); }

/* ── Kohdelista ────────────────────────────────────────────────────────── */
.list { display: flex; flex-direction: column; gap: 8px; padding: 0 12px 24px; }
/* The count over the cards and the button under them that draws the next
   batch (piirraLisaa). The button keeps the list's gutters and its full width:
   it is where the thumb already is at the end of a scroll. */
.lista-maara { margin: 0 12px 8px; }
.lista-lisaa { margin: 0 12px 24px; }

/* ── Listan lataus ────────────────────────────────────────────────────────
   /api/kohteet rescans the photo tree and can take the better part of twenty
   seconds. Two things say so: a bar that runs, and cards with nothing written
   on them yet where the answer will be. Indeterminate on purpose — the server
   does not report progress, and a bar that pretends to would be a lie. */
.lataus-palkki {
  height: 3px; margin: 0 12px 8px; overflow: hidden; border-radius: 3px;
  background: color-mix(in srgb, var(--accent) 22%, transparent);
}
.lataus-palkki::after {
  content: ''; display: block; height: 100%; width: 35%;
  border-radius: 3px; background: var(--accent);
  animation: lataus-juoksu 1.15s ease-in-out infinite;
}
@keyframes lataus-juoksu {
  from { transform: translateX(-110%); }
  to { transform: translateX(400%); }
}
.kohde.luuranko { pointer-events: none; }
.luu-kuva, .luu-otsikko, .luu-rivi {
  display: block; border-radius: 6px;
  background: color-mix(in srgb, var(--text) 9%, transparent);
  background-image: linear-gradient(
    90deg,
    transparent 0%,
    color-mix(in srgb, var(--text) 7%, transparent) 50%,
    transparent 100%);
  background-size: 220% 100%; background-repeat: no-repeat;
  animation: luu-hohto 1.5s ease-in-out infinite;
}
.luu-kuvat { grid-area: photos; display: flex; gap: 8px; margin-bottom: 4px; }
.luu-kuva { width: 84px; height: 56px; border-radius: 8px; }
.luu-otsikko { grid-area: title; height: 14px; width: 62%; }
.luu-rivi { grid-area: cols; height: 11px; width: 38%; }
@keyframes luu-hohto {
  from { background-position: 160% 0; }
  to { background-position: -60% 0; }
}
/* A page that shimmers is a page some people cannot read. The shapes stay —
   they are the message — and only the movement stops. */
@media (prefers-reduced-motion: reduce) {
  .lataus-palkki::after { animation: none; width: 100%; opacity: .6; }
  .luu-kuva, .luu-otsikko, .luu-rivi { animation: none; background-image: none; }
}
/* The card is a <button> and the map link is an <a>; an <a> inside a <button>
   is invalid and would bubble its click into openKohde. So they are siblings in
   a flex row, and the link gets a tile of its own beside the card rather than an
   icon floating over its corner — with a glove on, at the kerb, "open the card"
   and "show me where this is" must not share an edge. */
.kohde-row { display: flex; gap: 8px; align-items: stretch; }
.kohde-row .kohde { flex: 1; min-width: 0; }

/* The tile is a tiny map: paper, a street grid, a park, and a pin standing on
   it. Drawn entirely in gradients, so 150 rows cost no requests and no images. */
.map-link {
  /* mapLink() overrides these per row from a hash of the address. */
  --map-x: 0px; --map-y: 0px; --park-x: 74%; --park-y: 24%;
  flex: 0 0 52px;
  position: relative;
  display: grid; place-items: center;
  border: 1px solid var(--border); border-radius: var(--radius);
  text-decoration: none; overflow: hidden;
  background-color: var(--map-paper);
  background-image:
    repeating-linear-gradient(90deg, var(--map-road) 0 3px, transparent 3px 17px),
    repeating-linear-gradient(0deg, var(--map-road) 0 3px, transparent 3px 21px),
    radial-gradient(11px 13px at var(--park-x) var(--park-y), var(--map-park) 99%, transparent);
  background-position: var(--map-x) 0, 0 var(--map-y), 0 0;
  background-repeat: repeat, repeat, no-repeat;
  transition: border-color .15s ease, filter .15s ease;
}
.map-link:hover, .map-link:focus-visible {
  border-color: var(--accent);
  filter: saturate(1.2) brightness(1.04);
}

/* The tap is "drive me there", not "show me where", so the tile draws the last
   leg of a route arriving at the pin's tip: two borders of an invisible box,
   rounded at the corner. It runs off the bottom edge on purpose — a route comes
   from somewhere — and `overflow: hidden` on the tile does the cutting. */
.map-link::before {
  content: '';
  position: absolute;
  top: calc(50% + 8px); left: 10px;
  width: 16px; height: 40px;
  border-left: 3px solid var(--map-route);
  border-top: 3px solid var(--map-route);
  border-top-left-radius: 9px;
}

/* The classic teardrop: a square with three rounded corners, turned 45° so the
   square one points down. The rotation is why the hover offset looks odd — it
   is measured in the pin's own frame, where (4, -4) is straight up on screen. */
.map-pin {
  position: relative;
  width: 15px; height: 15px;
  border-radius: 50% 50% 50% 0;
  background: var(--map-pin);
  transform: rotate(-45deg) translate(1px, -1px);
  box-shadow: 0 3px 5px rgba(0, 0, 0, .35);
  transition: transform .18s cubic-bezier(.34, 1.56, .64, 1);
}
.map-pin::after {
  content: ''; position: absolute; left: 50%; top: 50%;
  width: 5px; height: 5px; margin: -2.5px 0 0 -2.5px;
  border-radius: 50%; background: var(--map-paper);
}
.map-link:hover .map-pin, .map-link:focus-visible .map-pin {
  transform: rotate(-45deg) translate(4px, -4px);
}
@media (prefers-reduced-motion: reduce) {
  .map-link, .map-pin { transition: none; }
}
/* ── Kohdekortti (P1, toinen kierros) ─────────────────────────────────────
   Top to bottom at every width: the photos, the address and the taloyhtiö, the
   columns, the badges. On a phone the columns are folded away behind a button
   (renderKohteet in app.js has the why); from 700px they are always open.

   NOTHING in this card clips or ellipsises. A value that does not fit wraps and
   the card grows — a list you scan for missing data cannot also hide data, and
   an "…" is indistinguishable from a value that happens to end there. Every
   width rule below is therefore about WHERE a line breaks, never about hiding
   what is past the edge.

   Rows are spaced with MARGINS, not with the grid's row-gap. Half the rows here
   are empty on any given card — no photos, the unfold button not shown from
   700px — and a grid still lays a gap between empty tracks, which read as a
   stray blank band at the top of 53 photo-less cards. */
.kohde {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas:
    "photos"
    "title"
    "cols"
    "badges"
    "toggle";
  column-gap: 12px; row-gap: 4px;
  padding: 12px 14px; text-align: left;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  min-height: 64px; align-items: start;
}

/* The card used to be this button. It stopped being one when the row grew a
   carousel: a <button> inside a <button> is invalid HTML and its click bubbles
   out, so tapping a photo would have opened the kohde. Instead the button holds
   only the address and the name, and stretches an empty ::after across the whole
   card — one tab stop, one accessible name — while the few controls that must
   catch their own taps are painted above it with z-index. */
.kohde-open {
  grid-area: title;
  /* A column on a phone — the address on its own line, the taloyhtiö under it —
     and one row from 700px. Wrapping either way: an address is never squeezed
     into "Furubor…" to keep a name beside it. */
  display: flex; flex-direction: column; align-items: flex-start;
  flex-wrap: wrap; column-gap: 10px; row-gap: 1px;
  min-width: 0; min-height: 0;
  padding: 0; border: 0; background: none; text-align: left; color: inherit;
}
.kohde-open::after {
  content: ''; position: absolute; inset: 0;
  border-radius: inherit;
  transition: background-color .12s ease;
}
/* Feedback on the whole card rather than the 1px nudge a plain button gets:
   the nudge would move the address and leave the badges and photos behind. */
.kohde-open:active { transform: none; }
.kohde-open:active::after { background: color-mix(in srgb, var(--text) 6%, transparent); }
/* The ring belongs on the card, and the overlay already traces it — so the
   button's own outline is moved to the ::after instead of needing :has(). */
.kohde-open:focus-visible { outline: none; }
.kohde-open:focus-visible::after { outline: 2px solid var(--accent); outline-offset: -1px; }

.kohde .addr { font-weight: 600; font-size: 15px; min-width: 0; overflow-wrap: anywhere; }
.kohde .owner { font-size: 13px; color: var(--muted); min-width: 0; overflow-wrap: anywhere; }
/* A property id or a Y-tunnus is never broken at its dashes. Lifted above the card's
   ::after so its tooltip (which number it is) shows; a tap still opens the kohde,
   the span being inside the open-button. */
.kohde .owner .k-tunnus {
  position: relative; z-index: 1;
  white-space: nowrap; font-variant-numeric: tabular-nums;
}

/* ── Kortin pää (2026-10-07) ──────────────────────────────────────────────
   The kohde number and the offer number at the head of the card, the two map
   links at the end of their row, the address and the company under them. No
   `position` on the wrapper: the open-button's ::after must keep spanning the
   whole card, and only the numbers and the links are lifted above it. */
.k-otsikko {
  grid-area: title;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "numerot linkit"
    "avaa avaa";
  align-items: center;
  column-gap: 10px; row-gap: 2px;
  min-width: 0;
}
.k-otsikko .kohde-open { grid-area: avaa; }
.k-numerot {
  grid-area: numerot;
  display: flex; flex-wrap: wrap; align-items: center; gap: 0 10px;
  min-width: 0;
}
.k-pikalinkit { grid-area: linkit; display: flex; gap: 6px; }

/* Big and bold, and a tap copies (kopioi() in app.js): a button, painted above
   the card's ::after, that reads as the number it is. */
.k-numero {
  position: relative; z-index: 1;
  font-size: 18px; font-weight: 700; line-height: 1.2;
  font-variant-numeric: tabular-nums;
}
button.k-numero {
  min-height: 36px; padding: 0 6px; margin: 0 -6px;
  border-color: transparent; border-radius: 8px; background: none;
}
button.k-numero:hover, button.k-numero:focus-visible { background: var(--surface-2); }
.k-numero.tyhja { color: var(--muted); font-weight: 400; }
/* "NRO" in front of the kohde number, as the card's column labels read. */
.k-numero .lab {
  margin-right: 5px;
  font-size: 10px; font-weight: 400; letter-spacing: .04em; text-transform: uppercase;
  color: var(--muted);
}
/* Where nothing can reach the clipboard: the number selected in place. */
.k-numero-valittu {
  position: relative; z-index: 1;
  min-height: 36px; padding: 0 6px;
  border: 1px solid var(--accent); border-radius: 8px;
  background: var(--surface);
  font-weight: 700; font-variant-numeric: tabular-nums;
}
/* The hidden copy of the number that execCommand('copy') copies from. */
.kopio-alue { position: fixed; top: 0; left: 0; opacity: 0; pointer-events: none; }

/* The city map and Google Maps: a square icon each, a new tab each. */
.k-pikalinkki {
  position: relative; z-index: 1;
  display: inline-grid; place-items: center;
  min-width: 40px; min-height: 40px;
  border: 1px solid var(--border); border-radius: 10px;
  background: var(--surface); color: var(--accent);
  transition: border-color .15s ease;
}
.k-pikalinkki:hover, .k-pikalinkki:focus-visible { border-color: var(--accent); }

/* Where a number came from ("Helsingin rekisteri"), after the number, on the
   card and in the facts block alike. */
.lahde { margin-left: 4px; font-size: 11px; font-weight: 400; color: var(--muted); }
.lahde::before { content: '· '; }

/* The pre-P1 badge column, restored: pills stacked on the right and allowed to
   wrap. Sized by its content, so on a narrow phone the widest pill ("⚠
   vahvistus puuttuu") plus the address's longest word can be wider than the
   card; letting the column shrink and its pills wrap is what keeps them inside
   the rounded corner — measured over the 208-row 00980 list, total list height
   was identical at 360px and up, and four rows stopped spilling. */
.kohde .badges {
  grid-area: badges;
  display: flex; flex-direction: row; flex-wrap: wrap; gap: 4px; align-items: flex-start;
  min-width: 0;
}
.kohde .badge { white-space: normal; text-align: right; }
.badge { font-size: 12px; padding: 2px 8px; border-radius: 999px; background: var(--surface-2); color: var(--muted); white-space: nowrap; }
.badge.done { background: color-mix(in srgb, var(--ok) 18%, transparent); color: var(--ok); }
.badge.offer { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent); }

/* z-index, not DOM order, is what makes the strip tappable: a positioned
   ::after paints above every non-positioned element of the card whatever their
   order, so only what is lifted above it catches its own taps. `minmax(0, …)`
   on the track and `min-width: 0` here keep 20 tiles inside the card instead of
   stretching the row — the strip scrolls, the card does not grow.
   Qualified by .kohde on purpose. `.carousel` is declared further down the
   file, so at equal specificity it would win every property the two share —
   padding, background and the bottom border came back and drew a stray rule
   under the tiles. A modifier has to outrank its base, not merely follow it. */
.kohde .row-carousel {
  grid-area: photos;
  position: relative; z-index: 1;
  min-width: 0; max-width: 100%;
  margin: 0 0 10px; padding: 0;
  background: transparent; border-bottom: 0;
  scroll-snap-type: none;
  /* Hidden, not styled thin: a scrollbar track inside the card reads as a stray
     rule under the tiles, and it costs 8px of a 62px strip. The tile clipped at
     the card's edge is the affordance — the hero's own strip, which has room,
     keeps its bar. */
  scrollbar-width: none;
}
.kohde .row-carousel::-webkit-scrollbar { display: none; }
/* 84px like every other strip, except where that lands a whole number of tiles
   exactly on the card's inner width — which it does at a 390px viewport, an
   iPhone 13/14 Pro, where the strip then showed three photos of fifteen with
   nothing peeking past the edge and read as complete. Below ~412px the fraction
   is the smaller of the two and a quarter of the fourth tile stays on screen;
   above it the card is wide enough that a tile is clipped anyway. min() rather
   than a media query because what matters is the card's width, not the phone's,
   and the map tile beside it makes those two different numbers. */
/* The +/- pair beside "Näytä numerot" scales this whole expression instead of
   replacing it, so the tuned relationship above survives at every step and the
   default of 1 renders exactly what it rendered before the control existed. */
.kohde .row-carousel .thumb { flex: 0 0 calc(min(84px, calc((100% - 24px) / 3.25)) * var(--kuva-kerroin, 1));
                              height: calc(62px * var(--kuva-kerroin, 1)); }

/* ── Sarakkeet ────────────────────────────────────────────────────────────
   Two lines: the people, then the building. A missing value is an em dash that
   never gives way — it is the thing being scanned for. On a phone they are
   folded until asked for; unfolded, the card grows as tall as the values need.
   From 700px they are always open, and a value too long for the line wraps
   rather than being cut: the card is allowed to be two lines taller, and it is
   never allowed to be a line the reader cannot finish. */
.k-lines { grid-area: cols; min-width: 0; display: none; gap: 4px; }
.kohde.auki .k-lines { display: grid; }
.k-line {
  display: flex; flex-wrap: wrap; align-items: baseline;
  column-gap: 12px; row-gap: 3px;
  min-width: 0;
  font-size: 13px; line-height: 1.45;
}
/* On a phone each of IS TSTO / IS / HUOLTO gets a line of its own: they are the
   three longest values on the card — a company name, a person, a company and a
   person — and side by side they wrap into each other until nobody can tell
   which half belongs to which label. The building line keeps flowing: its
   values are a word or a number each, and they read fine three or four to a
   line. From 700px both are single rows again. */
.k-people { flex-direction: column; }
/* The field's own contents flow as TEXT, not as flex items. As flex items a
   long name wrapped inside its own box and the number stayed up on the first
   line — "Aurinkolahden Huolto 050 401 3983 / Oy · Teppo Kohonen", which reads
   as the number belonging to the first half of the name. Flowing them means the
   number follows the END of the name and wraps with it when the card runs out
   of width, which is what was asked for. The LINE is still a flex row, so the
   fields keep sitting side by side. */
.k-line > .kentta { display: inline-block; min-width: 0; max-width: 100%; }
.k-line .lab {
  margin-right: 5px;
  font-size: 10px; letter-spacing: .04em; text-transform: uppercase;
  color: var(--muted);
}
.k-line .arvo { min-width: 0; overflow-wrap: anywhere; }
/* "We do not know" has to look different from "this is short", or the hole the
   estimator is scanning for reads as a value. */
.k-line .arvo.tyhja { color: var(--muted); opacity: .5; }

/* ── Numerot ──────────────────────────────────────────────────────────────
   Beside the name it belongs to, one flex item after the value, so it sits to
   its right and wraps onto the next line with it when the card runs out of
   width. Never broken across lines itself — half a phone number is worse than
   none. Hidden until the filter row's switch is on: a class on <body>, so one
   press reaches every card without re-rendering any of them.
   Lifted above the card's ::after, like the tiles, because a number is a
   tel: link and tapping it must dial rather than open the kohde. */
.k-line .numero {
  display: none;
  margin-left: 5px;
  position: relative; z-index: 1;
  white-space: nowrap; font-weight: 600; font-variant-numeric: tabular-nums;
}
body.numerot .k-line .numero { display: inline; }

/* The phone-width button that unfolds the columns — see avausNappi(). */
button.k-avaa {
  grid-area: toggle;
  position: relative; z-index: 1;
  min-height: 36px; padding: 0 12px;
  font-size: 13px; color: var(--muted);
  border: none;
}
button.k-avaa::after { content: ' ▾'; }
.kohde.auki button.k-avaa::after { content: ' ▴'; }

/* From 700px: the photos across the top, the address and the taloyhtiö on ONE
   line, the two column lines each as one row, the badges under them. The columns
   are always open, so the unfold button is not shown. */
@media (min-width: 700px) {
  .kohde {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "photos"
      "title"
      "cols"
      "badges";
    row-gap: 4px;
  }
  /* The head of the card on one row: the numbers, the address and the company,
     the map links at the end. */
  .k-otsikko {
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas: "numerot avaa linkit";
    column-gap: 14px;
  }
  /* In the middle column the address wraps rather than running under the links. */
  .k-otsikko .kohde-open .addr { flex: 0 1 auto; }
  /* Side by side, and wrapping when the two do not fit — the taloyhtiö moves
     under the address instead of being cut short of its own name. */
  .kohde-open { flex-direction: row; align-items: baseline; }
  .kohde .addr { flex: 0 0 auto; }
  .kohde .owner { flex: 0 1 auto; }

  .k-lines, .kohde.auki .k-lines { display: grid; }
  button.k-avaa { display: none; }

  /* Both column lines are rows here, wrapping onto a second line when the
     values need it. No overflow rule of any kind: the line is read ACROSS, and
     a card that is one line taller is a far smaller price than a value the
     reader cannot finish. */
  .k-line, .k-people { flex-direction: row; flex-wrap: wrap; }
  .k-line > .kentta { flex: 0 1 auto; }
}

/* ── Soittolista (P4) ──────────────────────────────────────────────────── */
/* The manager's name on a card is a button painted above the card's open
   overlay, like the tiles and "Näytä numerot": a long press on it opens the
   manager's call list and must not open the kohde. It keeps looking like the
   text it was — a dotted underline is the only hint — and it refuses the text
   selection and the iOS callout a long press would otherwise start. */
/* Both call-list entries: the manager and the office. They must read as the text
   they replace, not as buttons — the row is already dense. */
.k-line button.is-nimi,
.k-line button.is-tsto {
  position: relative; z-index: 1;
  min-height: 0; padding: 0; border: 0; background: none;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
  text-decoration: underline dotted; text-underline-offset: 3px;
  -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
  touch-action: manipulation;
}
.k-line button.is-nimi:active,
.k-line button.is-tsto:active { transform: none; }

.is-soita {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px;
  padding: 10px 16px 0;
}
.is-soita .numero { font-weight: 600; font-size: 17px; }
.is-soita p { flex-basis: 100%; margin: 0; }
.is-ryhma { margin: 14px 16px 6px; font-size: 14px; font-weight: 600; color: var(--muted); }

/* The answers form spans the whole card and sits above its open overlay, so a
   tap into a field types instead of opening the kohde. */
.kohde .pikakirjaus {
  grid-column: 1 / -1; position: relative; z-index: 1;
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px;
  margin-top: 6px; padding-top: 8px; border-top: 1px dashed var(--border);
}
.pikakirjaus label { display: flex; flex-direction: column; gap: 2px; font-size: 12px; color: var(--muted); }
.pikakirjaus input { width: 7.5em; }

/* ── Piirtoalue ────────────────────────────────────────────────────────── */
.stage {
  position: relative;
  height: 46dvh;
  min-height: 260px;
  background: #000;
  overflow: hidden;
  touch-action: none;      /* we handle pan/zoom/draw ourselves */
  user-select: none;
  -webkit-user-select: none;
}
@media (min-width: 700px) { .stage { height: 56dvh; } }

.stage-inner { position: absolute; inset: 0; transform-origin: 0 0; will-change: transform; }
#photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; pointer-events: none; }
#overlay { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
#overlay rect { fill: color-mix(in srgb, currentColor 18%, transparent); stroke: currentColor; stroke-width: 3; vector-effect: non-scaling-stroke; }
#overlay .sel rect { stroke-width: 5; stroke-dasharray: 8 4; }
#overlay text { fill: #fff; font-size: 26px; font-weight: 700; paint-order: stroke; stroke: #000a; stroke-width: 5px; }

.stage-empty {
  position: absolute; inset: 0; display: none;
  flex-direction: column; gap: 12px; align-items: center; justify-content: center;
  color: #cbd5e1; text-align: center; padding: 24px;
}
.stage.empty .stage-empty { display: flex; }
.stage.empty .stage-inner { display: none; }

.stage-tools { position: absolute; right: 8px; bottom: 8px; display: flex; gap: 6px; }
.tool { min-height: var(--tap); min-width: var(--tap); padding: 0 10px; border-radius: var(--radius);
        background: #000a; color: #fff; border-color: #fff3; font-size: 14px; white-space: nowrap;
        backdrop-filter: blur(4px); }
.tool[aria-pressed="true"] { background: var(--accent); border-color: transparent; }
.draw-hint { position: absolute; left: 50%; top: 12px; transform: translateX(-50%); margin: 0;
             background: #000a; color: #fff; padding: 6px 12px; border-radius: 999px; font-size: 13px; }

/* ── Karuselli ─────────────────────────────────────────────────────────── */
.carousel {
  display: flex; gap: 8px; padding: 8px 12px;
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  scroll-snap-type: x mandatory;
  background: var(--surface); border-bottom: 1px solid var(--border);
}
.thumb {
  position: relative; flex: 0 0 auto; width: 84px; height: 62px; padding: 0;
  border-radius: 8px; overflow: hidden; scroll-snap-align: start; min-height: 0;
  border: 2px solid transparent; background: var(--surface-2);
}
.thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.thumb.active { border-color: var(--accent); }
.thumb .n { position: absolute; right: 3px; bottom: 3px; background: #000b; color: #fff;
            font-size: 11px; padding: 0 5px; border-radius: 6px; }

/* ── Kuvan suurennos (hero) ────────────────────────────────────────────── */
/* Not a .sheet: a sheet rises from the bottom and leaves the list showing,
   which is right for a form and wrong for a photo. On a phone this takes the
   whole screen; from 720px — the same breakpoint where sheets become centred
   dialogs — it becomes a modal over a dimmed list. */
.hero { position: fixed; inset: 0; z-index: 60; display: flex; }
.hero[hidden] { display: none; }
/* The list would otherwise keep scrolling under the overlay, and closing the
   hero would land the estimator somewhere else in 200 rows than they left. */
body.hero-open { overflow: hidden; }
.hero-backdrop { position: absolute; inset: 0; background: #000c; }

.hero-panel {
  position: relative;
  display: flex; flex-direction: column; min-height: 0;
  width: 100%; height: 100%;
  background: var(--bg);
  padding: calc(env(safe-area-inset-top, 0px) + 8px) 8px calc(var(--safe-b) + 8px);
  animation: hero-in .16s ease-out;
}
@keyframes hero-in { from { opacity: .3; } }

/* Top right, over the photo and clear of the notch. Dark chip rather than the
   usual .ghost icon: it sits on whatever the photo happens to be. */
.hero-close {
  position: absolute; z-index: 2;
  top: calc(env(safe-area-inset-top, 0px) + 8px); right: 8px;
  min-width: var(--tap); min-height: var(--tap); padding: 0;
  border: 0; border-radius: 8px;
  background: #000a; color: #fff; font-size: 20px; line-height: 1;
}

.hero-stage {
  position: relative; flex: 1 1 auto; min-height: 0;
  touch-action: pan-y;  /* horizontal drags are ours: they step the photo */
}
/* Absolutely positioned rather than a centred grid item with max-height: 100%,
   which is the flexbox percentage trap — the stage's height comes from `flex`,
   so the percentage had nothing definite to resolve against and a portrait photo
   grew past the panel, over the counter and under the strip. Same treatment as
   #photo on the drawing stage, for the same reason. */
#hero-img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: contain; display: block;
}

.hero-nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  min-width: var(--tap); min-height: var(--tap); padding: 0;
  border: 0; border-radius: 8px;
  background: #000a; color: #fff; font-size: 28px; line-height: 1;
}
.hero-nav[hidden] { display: none; }
.hero-nav:active { transform: translateY(-50%) translateY(1px); }
.hero-nav.prev { left: 6px; }
.hero-nav.next { right: 6px; }

.hero-counter { margin: 8px 0 0; text-align: center; font-size: 13px;
                color: var(--muted); font-variant-numeric: tabular-nums; }

/* Same tiles as every other strip; the chrome around them is not wanted here. */
.hero-strip {
  flex: 0 0 auto;
  margin-top: 6px; padding: 2px 0;
  background: transparent; border-bottom: 0;
  scroll-snap-type: none; scrollbar-width: thin;
}

@media (min-width: 700px) {
  .hero { align-items: center; justify-content: center; padding: 24px; }
  .hero-panel {
    width: min(700px, 100%); height: min(88dvh, 100%);
    padding: 12px;
    border: 1px solid var(--border); border-radius: var(--radius);
    box-shadow: 0 24px 60px #0009;
  }
  .hero-close { top: 12px; right: 12px; }
  @keyframes hero-in { from { transform: scale(.97); opacity: .3; } }
}

@media (prefers-reduced-motion: reduce) {
  .hero-panel { animation: none; }
  .kohde-open::after { transition: none; }
}

/* ── Pelastustie-/rajakuva (S2.3) ──────────────────────────────────────── */
/* Its own strip rather than another icon on the photo: this answers "is this the
   right building?", which is a different question from "what work is on this
   photo?", and a missing answer has to be visible without hunting for it. */
.role-bar {
  display: flex; gap: 10px; align-items: center;
  margin: 10px 12px 0; padding: 8px 10px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
}
.role-bar[hidden] { display: none; }
.role-bar.missing { border-color: color-mix(in srgb, var(--warn) 55%, var(--border)); }
.role-bar .role-icon { font-size: 20px; line-height: 1; flex: 0 0 auto; }
.role-bar .role-text { flex: 1; min-width: 0; line-height: 1.25; }
.role-bar .role-text b { display: block; font-size: 14px; }
.role-bar .role-text span { font-size: 12px; color: var(--muted);
                            overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
.role-bar.missing .role-text b { color: var(--warn); }
.role-bar button { min-height: 38px; padding: 0 12px; font-size: 14px; white-space: nowrap; }
.role-bar .role-thumb {
  flex: 0 0 auto; width: 46px; height: 36px; padding: 0; min-height: 0;
  border-radius: 8px; overflow: hidden; border: 1px solid var(--border);
}
.role-bar .role-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.thumb .role { position: absolute; left: 3px; bottom: 3px; font-size: 11px;
               background: #000b; border-radius: 6px; padding: 0 4px; }
.badge.warn { background: color-mix(in srgb, var(--warn) 18%, transparent); color: var(--warn); }

/* ── Kuvien lisäys, sivumerkit ja arkistointi (2026-09-07) ─────────────── */
/* Top corners of a thumb: the side letter (P/I/E/L) and the source (LA/SV/MP/3D/📷).
   Bottom corners were already taken by the role and the box count. */
.thumb .side { position: absolute; left: 3px; top: 3px; background: #000b; color: #fff;
               font-size: 11px; font-weight: 700; padding: 0 5px; border-radius: 6px; }
.thumb .src  { position: absolute; right: 3px; top: 3px; background: #000b; color: #fff;
               font-size: 10px; padding: 0 4px; border-radius: 6px; letter-spacing: .02em; }
.thumb.archived img { opacity: .4; filter: grayscale(.7); }
.thumb.archived { border-style: dashed; border-color: var(--border); }
.thumb.archived.active { border-color: var(--accent); }
/* Valintatila: peukalonkuvasta tulee valintaruutu, ei avainta kuvaan. */
.thumb.picked { border-color: var(--accent); }
.thumb.picked img { opacity: .55; }
.thumb .pick { position: absolute; inset: 0; display: grid; place-items: center;
  font-size: 22px; color: #fff; text-shadow: 0 0 4px #000; }
.thumb-add, .thumb-more {
  display: grid; place-items: center; border: 2px dashed var(--border);
  background: transparent; color: var(--muted);
}
.thumb-add { font-size: 30px; line-height: 1; }
.thumb-add:disabled { opacity: .5; }
.thumb-more { font-size: 12px; line-height: 1.2; padding: 0 6px; white-space: normal; text-align: center; }

.missing-sides { display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
                 padding: 8px 12px 0; background: var(--surface); }
.missing-sides[hidden] { display: none; }
.missing-sides .label { font-size: 12px; font-weight: 600; color: var(--warn); }
.missing-sides .chip { min-height: 32px; padding: 0 10px; font-size: 13px; }
.chip.warn { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 55%, var(--border)); }

.photo-bar {
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  margin: 8px 12px 0; padding: 8px 10px; font-size: 13px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
}
.photo-bar[hidden] { display: none; }
.photo-bar .pb-side { font-size: 15px; min-width: 1.2em; }
.photo-bar .pb-text { flex: 1 1 160px; min-width: 0; color: var(--muted);
                      overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.photo-bar .pb-status { font-size: 12px; padding: 2px 8px; border-radius: 999px; background: var(--surface-2); }
.photo-bar .pb-status.ok { background: color-mix(in srgb, var(--ok) 18%, transparent); color: var(--ok); }
.photo-bar .pb-status.partial, .photo-bar .pb-status.malli {
  background: color-mix(in srgb, var(--warn) 18%, transparent); color: var(--warn);
}
.photo-bar button { min-height: 38px; padding: 0 12px; font-size: 14px; white-space: nowrap; margin-left: auto; }

.side-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
.side-chips .chip { min-height: var(--tap); min-width: 64px; }

/* ── Arviorivit ────────────────────────────────────────────────────────── */
.rows { flex: 1 1 auto; display: flex; flex-direction: column; gap: 6px; padding: 10px 12px 4px; }
@media (min-width: 1100px) { .rows { flex: 0 0 auto; padding: 0; } }
.row {
  /* stripe | number | name | hours — the number is the label drawn on the box. */
  display: grid; grid-template-columns: 6px auto 1fr auto; gap: 10px; align-items: center;
  padding: 10px 12px 10px 0; min-height: 52px; text-align: left;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  overflow: hidden;
}
.row .stripe { width: 6px; height: 100%; min-height: 52px; background: currentColor; }
.row .name { min-width: 0; }
.row .name b { display: block; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row .name span { font-size: 12px; color: var(--muted); }
.row .h { font-variant-numeric: tabular-nums; font-weight: 700; white-space: nowrap; padding-right: 4px; }
.rows-empty { padding: 18px 4px; color: var(--muted); font-size: 14px; }

/* ── Vanha tarjous ─────────────────────────────────────────────────────── */
.old-offer { margin: 4px 12px 12px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); }
.old-offer summary { padding: 12px 14px; cursor: pointer; min-height: var(--tap); display: flex; align-items: center; gap: 8px; }
.old-offer table { width: 100%; border-collapse: collapse; font-size: 13px; }
.old-offer td { padding: 5px 14px; border-top: 1px solid var(--border); }
.old-offer td.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.old-offer tr.hdr td { font-weight: 600; color: var(--muted); }
.offer-attach { display: flex; gap: 8px; padding: 10px 14px; border-top: 1px solid var(--border); }
.offer-attach input { flex: 1 1 auto; min-width: 0; }
.offer-attach button { white-space: nowrap; }
#offer-detach { margin: 0 14px 10px; }

/* ── Yhteenveto ────────────────────────────────────────────────────────── */
.totals {
  position: sticky; bottom: 0; z-index: 15;
  padding: 10px 16px calc(10px + var(--safe-b));
  background: var(--surface); border-top: 1px solid var(--border);
}
.totals-main { display: flex; justify-content: space-between; font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; }

/* ── Alapaneelit ───────────────────────────────────────────────────────── */
.sheet { position: fixed; inset: 0; z-index: 40; }
.sheet[hidden] { display: none; }
.sheet-backdrop { position: absolute; inset: 0; background: #0008; }
.sheet-panel {
  position: absolute; left: 0; right: 0; bottom: 0;
  max-height: 88dvh; overflow-y: auto;
  padding: 8px 16px calc(16px + var(--safe-b));
  background: var(--surface);
  border-radius: 18px 18px 0 0;
  animation: rise .18s ease-out;
}
.sheet-panel.tall { height: 88dvh; }
@media (min-width: 720px) {
  .sheet-panel { left: 50%; right: auto; transform: translateX(-50%); width: 560px; border-radius: 18px 18px 0 0; }
}
@keyframes rise { from { transform: translateY(24px); opacity: .4; } }
@media (min-width: 720px) { @keyframes rise { from { transform: translate(-50%, 24px); opacity: .4; } } }
.sheet-grip { width: 44px; height: 4px; margin: 4px auto 10px; border-radius: 2px; background: var(--border); }
.sheet-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; }
.sheet-actions { display: flex; gap: 10px; margin-top: 20px; }
.sheet-actions .primary { flex: 1; }

.picker { max-height: 210px; overflow-y: auto; border: 1px solid var(--border); border-radius: var(--radius); margin-top: 6px; }
.picker button {
  display: flex; gap: 10px; align-items: baseline; width: 100%;
  border: 0; border-bottom: 1px solid var(--border); border-radius: 0;
  background: transparent; text-align: left; padding: 10px 12px; min-height: var(--tap);
}
.picker button:last-child { border-bottom: 0; }
.picker button.sel { background: color-mix(in srgb, var(--accent) 14%, transparent); }
.picker .code { font-variant-numeric: tabular-nums; color: var(--muted); font-size: 13px; min-width: 3.2em; }
.picker .use { margin-left: auto; font-size: 12px; color: var(--muted); }

.tt-picker { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
.tt-picker button { justify-content: flex-start; gap: 8px; display: flex; align-items: center; }
.tt-picker button[aria-pressed="true"] { border-color: currentColor; border-width: 2px;
  background: color-mix(in srgb, currentColor 16%, transparent); font-weight: 600; }
.tt-dot { width: 12px; height: 12px; border-radius: 50%; background: currentColor; flex: 0 0 auto; }

.hours-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.presets { display: flex; gap: 6px; width: 100%; overflow-x: auto; padding-bottom: 2px; }
.presets button { flex: 0 0 auto; min-width: 58px; padding: 0 12px; white-space: nowrap; }
.presets button.sel { background: var(--accent); color: var(--accent-text); border-color: transparent; }
.hours-free { display: flex; align-items: center; gap: 8px; }
.hours-free input { width: 104px; text-align: center; font-weight: 700; }
.hours-free .unit { color: var(--muted); font-size: 14px; }


.info { margin: 8px 0 0; }
.info dt { font-size: 12px; color: var(--muted); margin-top: 10px; }
.info dd { margin: 2px 0 0; }

/* ── Toast ─────────────────────────────────────────────────────────────── */
.toast {
  position: fixed; left: 50%; bottom: calc(84px + var(--safe-b)); transform: translateX(-50%);
  z-index: 60; max-width: 90vw;
  padding: 12px 18px; border-radius: 999px;
  background: #111827ee; color: #fff; font-size: 14px;
  box-shadow: 0 6px 24px #0006;
}
.toast[hidden] { display: none; }
/* The frame between showing the live region and writing its text (toast() in app.js). */
.toast:empty { opacity: 0; }
.toast.bad { background: #991b1bee; }

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


/* ── Leiskan kolme saraketta ───────────────────────────────────────────────
   Phone: one column, ordered photo → thumbnails → work panel → rows, so the
   panel that arms drawing is directly under the image it draws on.
   Desktop: tiedot | kuva | työt, as drawn. */

.kohde-grid { display: flex; flex-direction: column; min-height: 0; }
.col-left, .col-mid, .col-right { display: contents; }

#stage         { order: 1; }
#missing-sides { order: 2; }
#carousel      { order: 3; }
#photo-bar     { order: 4; }
#panel         { order: 5; }
#role-bar      { order: 6; }
#rows          { order: 7; }
#tietue-btn    { order: 8; }
#tietue-status { order: 9; }
#raakadata     { order: 10; }
#facts         { order: 11; }
#old-offer     { order: 12; }

@media (min-width: 1100px) {
  .kohde-grid {
    display: grid;
    grid-template-columns: minmax(260px, 320px) minmax(0, 1fr) minmax(280px, 340px);
    gap: 12px;
    padding: 0 12px;
    align-items: start;
  }
  .col-left, .col-mid, .col-right {
    display: flex;
    flex-direction: column;
    gap: 10px;
    min-width: 0;
  }
  /* Inside a column the source order is already right, so the phone `order`
     values must not leak in and shuffle it. */
  .col-left > *, .col-mid > *, .col-right > * { order: 0; margin-left: 0; margin-right: 0; }

  /* Both side columns stick and scroll INSIDE themselves. Without the height
     cap the work panel grew past the fold and "VALMIS = KIRJOITA TIETUE" ended
     up underneath the sticky totals bar — present in the DOM, unreachable with
     a mouse. The cap is the viewport minus the top bar and the totals bar. */
  .col-left, .col-right {
    position: sticky;
    top: 8px;
    max-height: calc(100dvh - var(--chrome-h));
  }
  /* The right column scrolls as a whole and every block in it keeps its full height
     (Jyri 9.10.2026: the side panel's blocks "во весь рост", not windows of their own).
     The button stays pinned to the bottom of the column while the rest scrolls under
     it, which is the whole point of putting it there. */
  .col-right { overflow-y: auto; overscroll-behavior: contain; }
  .col-right > * { flex-shrink: 0; }
  .col-right > #tietue-btn { position: sticky; bottom: 0; z-index: 2; }
  .col-left > .facts  { flex: 0 1 auto; min-height: 0; overflow-y: auto; }
}

/* ── Tietolohko (leiskan vasen ylä) ───────────────────────────────────── */

.facts, .raakadata, .panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin: 8px 12px 0;
  padding: 12px 14px;
}
@media (min-width: 1100px) { .facts, .raakadata, .panel { margin: 0; } }

.facts h2, .raakadata h2, .panel h2 {
  margin: 0 0 10px;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
}

.facts-list { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; margin: 0; }
.facts-list dt { font-size: 13px; color: var(--muted); align-self: center; }
.facts-list dd { margin: 0; font-size: 14px; min-width: 0; align-self: center; }
/* Kartta ja katunäkymä portaiden alla (2026-10-02): kaksi linkkiä rivissä. */
.karttalinkit { display: flex; flex-wrap: wrap; gap: 4px 14px; }
.karttalinkki { color: var(--accent); text-decoration: underline; }
/* The door photos' hint (sisaankaynnit, 2026-10-03): text, then a link-styled jump. */
.ovikuvat { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; }
.ovikuvat .linklike {
  min-height: 0; padding: 0; border: 0; background: none; font: inherit;
  color: var(--accent); text-decoration: underline; cursor: pointer;
}

.fact-edit {
  min-height: 32px; padding: 2px 8px; width: 100%; justify-content: flex-start;
  background: transparent; border: 1px dashed transparent; border-radius: 8px;
  font-size: 14px; text-align: left; color: inherit;
}
.fact-edit:hover, .fact-edit:focus-visible { border-color: var(--border); background: var(--surface-2); }
.fact-edit.empty { color: var(--muted); border-color: var(--border); }
.manual-dot { color: var(--accent); margin-left: 6px; font-weight: 700; }

/* The status row needs the whole width: four chips wrapping inside the value
   column pushed them onto the line above their own label. */
.facts-list dt.wide { grid-column: 1 / -1; margin-top: 8px; }
.facts-list dd.wide { grid-column: 1 / -1; }
.status-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 4px 0; }
.status-chips .chip { min-height: 30px; padding: 0 10px; font-size: 12px; }

/* ── Raakadata: vanha vs AI ───────────────────────────────────────────── */

.raaka { width: 100%; border-collapse: collapse; font-size: 13px; }
.raaka th { font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
  color: var(--muted); font-weight: 600; padding: 0 0 4px; text-align: left; }
.raaka td { padding: 5px 0; border-top: 1px solid var(--border); vertical-align: top; }
.raaka td.num, .raaka th.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; padding-left: 8px; }
.raaka td b { font-weight: 600; }
.raaka .auto { display: block; font-size: 11px; color: var(--muted); }
/* A difference past the configured threshold is the worklist, so it is the one
   thing in this table that has to be findable without reading it. */
.raaka tr.flag td { background: color-mix(in srgb, var(--warn) 12%, transparent); }
.raaka tr.none td b { color: var(--muted); text-decoration: line-through; }
.raaka tr.sum td { border-top: 2px solid var(--border); font-weight: 700; }

/* ── Suunnittele töitä ────────────────────────────────────────────────── */

.porras-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
.porras-label { font-size: 12px; color: var(--muted); }
.porras-row .chip { min-height: 32px; min-width: 34px; padding: 0 10px; }

.panel-cats { display: flex; flex-direction: column; gap: 4px; }
.cat { border: 1px solid transparent; border-radius: 10px; }
.cat.open { border-color: var(--border); background: var(--surface-2); }
.cat.none .cat-head .label { color: var(--muted); text-decoration: line-through; }

.cat-head {
  width: 100%; min-height: 40px; display: flex; align-items: center; gap: 8px;
  padding: 0 10px; background: transparent; border: 0; color: inherit;
  font-size: 14px; text-align: left;
}
.cat-head .code { font-variant-numeric: tabular-nums; color: var(--muted); font-size: 12px; min-width: 2em; }
.cat-head .label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cat-head .h { font-size: 12px; font-variant-numeric: tabular-nums; color: var(--ok); white-space: nowrap; }
.cat-head .h.none { color: var(--muted); }

.pinnat { display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px; padding: 4px 10px 10px; }
.pinnat .pinta { min-height: 38px; font-size: 13px; }
.pinnat .pinta.sel { background: var(--accent); color: var(--accent-text); border-color: transparent; }
.pinnat .pinta.none { grid-column: 1 / -1; color: var(--muted); }
.pinnat .pinta.none.sel { background: var(--surface); color: var(--muted); border-style: dashed; }

.old-text { padding: 0 10px 10px; font-size: 12px; color: var(--muted); }
.old-text-head { display: block; font-size: 11px; text-transform: uppercase;
  letter-spacing: .04em; margin-bottom: 3px; }
.old-text div { padding: 1px 0; }

.panel-hint { margin: 10px 0 0; font-size: 12px; }
.panel-hint.armed { color: var(--accent); font-weight: 600; }

.tietue { width: calc(100% - 24px); margin: 10px 12px 0; }
#tietue-status { margin: 4px 12px 12px; }
@media (min-width: 1100px) { .tietue { width: 100%; margin: 0; } #tietue-status { margin: 0; } }

/* ── Laatikon alapaneeli: valmiiksi täytetty työ ──────────────────────── */

.prefill {
  background: var(--surface-2); border-radius: var(--radius);
  padding: 10px 12px; margin-bottom: 12px;
}
.prefill-main { display: flex; align-items: center; gap: 8px; }
.prefill-sub { font-size: 12px; margin-top: 4px; }
.manual { margin-bottom: 6px; }
.manual > summary { min-height: var(--tap); display: flex; align-items: center;
  cursor: pointer; font-size: 14px; color: var(--muted); }

.warn {
  color: var(--warn); background: color-mix(in srgb, var(--warn) 14%, transparent);
}
/* The box around a warning, for everything EXCEPT the "⚠ vahvistus puuttuu"
   pill: that one is also `.badge`, and at equal specificity this later rule used
   to win and blow the pill up into an 8px-padded box. Scoped rather than removed
   so the hours warning in the box sheet (#box-hours-warn) keeps its padding. */
.warn:not(.badge) { margin: 6px 0 0; padding: 8px 10px; border-radius: 8px; font-size: 13px; }
.warn[hidden] { display: none; }

/* Box number, matching the one drawn on the photo. */
.row .num {
  font-variant-numeric: tabular-nums; font-weight: 700; color: var(--muted);
  min-width: 1.6em; text-align: right; font-size: 13px;
}

.badge.ero { background: var(--surface-2); font-variant-numeric: tabular-nums; }
.badge.ero.flag { background: color-mix(in srgb, var(--warn) 18%, transparent); color: var(--warn); font-weight: 600; }
.badge.status { background: color-mix(in srgb, var(--ok) 16%, transparent); color: var(--ok); }

/* Avoimen rekisterin kentät: näkyvät samassa listassa kuin käsin täytettävät,
   mutta eivät saa näyttää siltä että niitä voisi painaa — rivin kirjoittaa
   scripts/enrich_kohde_geo.py uusiksi, joten muokkaus katoaisi. */
.facts dd.from-register {
  color: var(--muted, #6b7280);
  font-variant-numeric: tabular-nums;
}
