/* Offer detail page (issue 00078) — the compare-workbench card language
   (docs/extraction/renders/offer_compare_template.html) adapted to a single
   offer: same palette/typography/radii, one column, no diffing chrome.
   Tokens live in theme.css (shared with the feed) — link it FIRST. */

* { box-sizing: border-box; }
body {
  margin: 0; background: var(--bg); color: var(--ink);
  font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.wrap { max-width: 680px; margin: 0 auto; padding: 20px 16px 80px; }

.card { background: var(--card); border: 1px solid var(--line); border-radius: 14px; overflow: hidden; }
.pad { padding: 18px; }
h1 { font-size: 20px; margin: 0 0 2px; font-weight: 650; letter-spacing: -0.01em; }
/* Header row (issue 00047): h1 left, display-currency picker right. */
.page-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.cur-picker {
  font: inherit; font-size: 13px; color: var(--ink-soft);
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 8px; padding: 4px 8px;
}
.cur-picker:disabled { opacity: 0.5; } /* no JS → nothing to rewrite */
.loc { color: var(--ink-soft); font-size: 14px; margin-bottom: 14px; overflow-wrap: anywhere; }
.miss { color: var(--miss); font-style: italic; font-size: 12.5px; }

.price-hero {
  border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px;
  background: var(--surface); margin-bottom: 14px;
}
.price-hero .allin { font-size: 12px; color: var(--ink-soft); margin-bottom: 2px; }
.price-hero .big {
  font-size: 26px; font-weight: 750; letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}
.price-hero .per { font-size: 13px; color: var(--ink-soft); font-weight: 400; }
/* Currency suggestion line (issue 00047, ADR-0008): always present on a
   priced hero as currency.js's rewrite target; hidden when empty. */
.price-hero .conv {
  font-size: 13px; color: var(--ink-soft); margin-top: 2px;
  font-variant-numeric: tabular-nums;
}
.price-hero .conv:empty { display: none; }
.price-hero .note { font-size: 12.5px; color: var(--ink-soft); margin-top: 6px; overflow-wrap: anywhere; }

.facts {
  display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--line);
  border: 1px solid var(--line); border-radius: 10px; overflow: hidden; margin-bottom: 16px;
}
.fact { background: var(--card); padding: 9px 12px; min-width: 0; }
.fact.wide { grid-column: 1 / -1; }
.fact .k { font-size: 11px; color: var(--ink-faint); margin-bottom: 1px; }
.fact .v { font-size: 13.5px; overflow-wrap: anywhere; }

/* Photo strip (issue 00086 / 00096) — the album above «Детали»: one
   horizontal scroll row, fixed height so mixed aspect ratios line up; absent
   entirely when the post has no fetched photos. Each shot is an <a> to the
   full image (no-JS fallback); offer.js opens the lightbox instead. */
.photos {
  display: flex; gap: 8px; overflow-x: auto; margin-bottom: 16px;
  scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch;
}
.photos .shot {
  flex: none; display: block; scroll-snap-align: start;
  border-radius: 10px; cursor: zoom-in;
}
.photos .shot img {
  height: 220px; width: auto; display: block;
  border: 1px solid var(--line); border-radius: 10px;
  background: var(--surface);
}

/* The video's strip tile (issue 00122, superseding 00110's standalone
   block): same fixed shot slot, poster image, with a centered play glyph —
   same visual language as the feed-card overlay (`li.offer .play-overlay`,
   feed.css) but a separate rule since this tile lives outside `li.offer`. */
/* `pointer`, not the photo tiles' `zoom-in`: the tile opens a player,
   nothing magnifies. */
.photos .shot.video-shot { position: relative; cursor: pointer; }
.photos .shot.video-shot .play-overlay {
  /* 2× the feed-card glyph: the 220px-tall strip tile is a far bigger
     canvas than the 96×72 card slot (user request 2026-07-24). */
  position: absolute; inset: 0; margin: auto; width: 56px; height: 56px;
  display: flex; align-items: center; justify-content: center;
  color: #fff; background: rgba(0, 0, 0, 0.45); border-radius: 999px;
  padding: 12px;
}

/* The dead-video tile (user direction 2026-07-27, after the archive-race
   incident 404ed a live offer's clip): video.js swaps the broken poster/play
   tile for this — the SAME framed shot slot, hatched like the pending tiles
   (one «media problem here» language, minus their pulse: nothing is coming
   without a deploy), with the explanation and the clip link inside. The
   tile stays the `<a href>` to the clip file. */
.photos .shot.video-dead {
  height: 220px; width: 165px; cursor: pointer; box-sizing: border-box;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; padding: 0 14px; text-align: center;
  font-size: 12.5px; color: var(--ink-soft); text-decoration: none;
  border: 1px solid var(--line);
  background: repeating-linear-gradient(
    45deg, var(--surface) 0 10px, var(--chip) 10px 20px);
}
.photos .shot.video-dead .dead-note { white-space: pre-line; }
.photos .shot.video-dead .dead-link { text-decoration: underline; color: var(--ink); }

/* The hatched, rounded frame is one «media problem here» surface, so every
   place that draws it draws it from HERE: the census placeholder for a photo
   still on the way (issue 00118), the tile for the photos the page has stopped
   promising (issue rl-pxaz), that tile's lightbox slide, and that slide's
   gallery-rail thumb (issue rl-2xvi, both sized far below). Add a member to
   this list rather than another copy of the gradient.

   `.lb-thumb.lb-thumb-unfetched` carries two classes on purpose: `.lb-thumb`
   sets `background: none` further down, and one class would lose the gradient
   to it. */
.photos .shot.pending, .photos .shot.unfetched, .lb-unfetched-note,
.lb-thumb.lb-thumb-unfetched {
  border-radius: 10px;
  border: 1px solid var(--line);
  background: repeating-linear-gradient(
    45deg, var(--surface) 0 10px, var(--chip) 10px 20px);
}
/* The two owed-photo TILES share one box on top of that surface: both take
   the shot slot of the dead-video tile. One selector, so the differences
   below are the only differences a reader can see. `font: inherit` and the
   reset of the button chrome are for the missing tile, which is a `<button>`
   since issue rl-2xvi. */
.photos .shot.pending, .photos .shot.unfetched {
  height: 220px; width: 165px; cursor: default; box-sizing: border-box;
  display: flex; align-items: center; justify-content: center;
  font: inherit; margin: 0;
  color: var(--ink-faint);
}
/* The pulse is the difference, and it is the whole distinction: it says the
   photo is on its way. The missing tile keeps its own class so that no later
   change to `.shot.pending` can animate it by accident. */
.photos .shot.pending {
  animation: assembling-pulse 1.6s ease-in-out infinite;
}
/* The missing tile carries words where the pending tile is empty, so it also
   carries what words need. */
.photos .shot.unfetched {
  padding: 0 14px; text-align: center;
  font-size: 12.5px; color: var(--ink-soft);
}
/* The refused clip's tile (issue rl-s5or) carries a glyph ABOVE its words,
   where the photo tile carries words alone — the same stack, and the same
   gap, as the dead-video tile above, because both say «this clip is not
   playing» in the same slot. */
.photos .shot.unfetched.video { flex-direction: column; gap: 10px; }
/* Since issue rl-2xvi the missing tile is a `<button>` that opens its own
   lightbox slide, so it takes the affordance of the video tile — a pointer,
   because it opens a panel and nothing magnifies. The `<span>` form (the zero
   capture, which draws no lightbox) keeps `cursor: default` from the shared
   rule above. */
.photos button.shot.unfetched { cursor: pointer; }
.note.assembling {
  font-size: 12.5px; color: var(--ink-faint); font-style: italic;
  margin: -8px 0 16px;
}
@keyframes assembling-pulse {
  0%, 100% { opacity: 0.55; }
  50% { opacity: 1; }
}

/* The native video player, now a lightbox slide (issue 00122, superseding
   00110's standalone `.video-wrap` block): no custom chrome around
   `<video>` itself — `controls` draws its own UI — just the corner
   duration/filesize badge. TOP-LEFT, not top-right: top-right collided with
   `.lb-close` (hit in the 2026-07-24 spike). */
.lb-slide.lb-video video {
  max-width: 100%; max-height: 100%;
  width: auto; height: auto;
  display: block; background: #000;
  /* The slide's zoom-out cursor (click-to-close affordance) must not
     bleed onto the player — a tap here is play/pause, same reset as
     `.lb-slide picture img` gives photos (user-caught 2026-07-24). */
  cursor: default;
}
.lb-slide.lb-video .video-badge {
  position: absolute; z-index: 1; top: 8px; left: 8px;
  font-size: 12px; font-variant-numeric: tabular-nums;
  color: #fff; background: rgba(0, 0, 0, 0.6);
  border-radius: 6px; padding: 2px 7px;
}
/* The strip tile carries the same badge BOTTOM-RIGHT (user request
   2026-07-24: the standalone block's always-visible duration/size must
   not be lost to the lightbox) — clear of the centered play glyph and
   of the tile's rounded corner. */
.photos .shot.video-shot .video-badge {
  position: absolute; z-index: 1; right: 8px; bottom: 8px;
  font-size: 12px; font-variant-numeric: tabular-nums;
  color: #fff; background: rgba(0, 0, 0, 0.6);
  border-radius: 6px; padding: 2px 7px;
}
/* No `picture img` ever sits inside this slide, so `lb-loaded` never fires
   (offer.js's `wireLoadState` is scoped to `picture img` and already skips
   it) — but without an explicit override the base `.lb-slide:not(.lb-loaded)
   :not(.lb-error)::after` rule would draw the photo-slide spinner over the
   video forever. Native `controls` already shows its own buffering state.
   The `:not()` chain is repeated to OUT-SPECIFY that base rule (0,4,0 vs
   0,3,0) — a bare `.lb-slide.lb-video::after` loses and the spinner stays
   (caught in the 2026-07-24 MANUAL-QA walk). */
.lb-slide.lb-video:not(.lb-loaded):not(.lb-error)::after { content: none; }
/* The zero-JS / decode-failure fallback card (issue 00110): rendered either
   as `<video>`'s inert inner content (no element support / JS off) or
   swapped in by video.js on a decode `error`. Same look either way. */
.video-fallback {
  border: 1px solid var(--line); border-radius: 10px; padding: 14px;
  background: var(--surface); font-size: 13.5px; margin-bottom: 16px;
}
/* pre-line: video.js writes the dead-link message with a \n line break. */
.video-fallback p { margin: 0 0 8px; white-space: pre-line; }
.video-fallback p:last-child { margin-bottom: 0; }

/* Lightbox overlay (issue 00096) — SSR'd hidden per offer, unhidden by
   offer.js. `hidden` + display:none keeps it inert (and out of the a11y tree)
   until opened; the [hidden] guard beats display:flex specificity. */
.lightbox {
  /* Above Leaflet: its control container is z-index 1000, which ties (and
     in Firefox wins over) a 1000 here — the map's +/− floated over the
     overlay. */
  position: fixed; inset: 0; z-index: 2000;
  /* Column layout: the stage takes all height left over by the in-flow
     gallery row, so the image can never slide under the gallery — it
     letterboxes between viewport top and gallery top (or fills edge to
     edge where the gallery is hidden). */
  display: flex; flex-direction: column; align-items: center;
}
.lightbox[hidden] { display: none; }
/* Page scroll lock while the dialog is open (offer.js toggles .lb-open). */
body.lb-open { overflow: hidden; }

.lb-backdrop {
  position: absolute; inset: 0;
  background: rgba(0, 0, 0, 0.82); cursor: zoom-out;
}
.lb-stage {
  position: relative; z-index: 1;
  display: flex; width: 100%; flex: 1 1 0; min-height: 0;
  transition: transform 0.28s ease;
  touch-action: pan-y;
}
.lb-stage.lb-instant { transition: none; }
.lb-slide {
  flex: 0 0 100%; width: 100%; height: 100%;
  display: flex; align-items: center; justify-content: center;
  /* No margin from the viewport: the image is as large as the slide
     allows, touching either top+gallery or the left+right edges. */
  padding: 0;
  /* The stage occludes .lb-backdrop, so click-to-close on the dimmed
     margin lives on the stage (offer.js); match the backdrop affordance. */
  cursor: zoom-out;
  /* Anchor for the absolutely-positioned .lb-ph loading placeholder. */
  position: relative;
}
/* Loading placeholder (user request 2026-07-22): the thumb variant fills
   the same contain box as the full image, SLIGHTLY dimmed — washed
   lighter on a light OS, darker on a dark one — under a spinner. The
   thumb previews well enough to be mistaken for the final image, so the
   spinner (not a breathing dim: opacity waves let the dimmed page bleed
   through) says "still loading" while letting the renter skip ahead
   without waiting. offer.js adds `lb-loaded` when the full image has
   decoded (placeholder and spinner drop) or `lb-error` when it failed
   (spinner drops, static thumb stays — a spinner promises progress, and
   there is none). */
.lb-slide .lb-ph {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%; object-fit: contain;
  filter: brightness(1.12) saturate(0.92);
}
@media (prefers-color-scheme: dark) {
  .lb-slide .lb-ph { filter: brightness(0.75) saturate(0.92); }
}
.lb-slide.lb-loaded .lb-ph { display: none; }
/* The spinner: a white arc over a translucent dark ring — readable on
   both the brightened (light OS) and darkened (dark OS) thumb. */
.lb-slide:not(.lb-loaded):not(.lb-error)::after {
  content: ""; position: absolute; z-index: 2;
  top: 50%; left: 50%; margin: -19px 0 0 -19px;
  width: 38px; height: 38px; border-radius: 50%;
  border: 3px solid rgba(0, 0, 0, 0.35);
  border-top-color: #fff;
  animation: lb-spin 0.9s linear infinite;
}
@keyframes lb-spin {
  to { transform: rotate(360deg); }
}
/* Functional motion (progress), so not removed outright — just calmed. */
@media (prefers-reduced-motion: reduce) {
  .lb-slide:not(.lb-loaded):not(.lb-error)::after { animation-duration: 2.5s; }
}
/* The missing photo's slide (issue rl-2xvi): the strip tile's panel, grown to
   the space a photo takes. Bare words centred on the backdrop read as a
   rendering fault — nothing occupies the slide, and the photo behind shows
   past them. The hatched frame is the same sentence the strip tile speaks:
   the slot is here, the photo is not. The hatch comes from the shared surface
   rule above; only the size and the words are local.

   Sized against the SLIDE (percentages, and the slide is a definite box), not
   the viewport: the panel letterboxes the way a photo does, keeps its margin
   of dimmed backdrop at desktop, and cannot outgrow a phone screen. The caps
   stop it from becoming a billboard on a wide monitor. */
.lb-unfetched-note {
  box-sizing: border-box;
  width: min(82%, 700px); height: min(78%, 525px);
  display: flex; align-items: center; justify-content: center;
  padding: 0 24px; text-align: center;
  font-size: 15px; line-height: 1.5;
  /* Page ink, not white: the hatch is a page-theme surface (`--surface` /
     `--chip`), so it is light on a light OS and dark on a dark one. White
     text held over the light hatch is what `--ink-soft` avoids — it flips
     with the hatch, exactly as it does on the strip tile. */
  color: var(--ink-soft);
}
/* The placeholder wears `.video` where the video pass failed (issues rl-wfeh
   and rl-lf12), and it then carries the play glyph ABOVE its words — the same
   stack, and the same reason, as the `.shot.unfetched.video` strip tile: the
   glyph says a CLIP is among the media the words name. The gap is the slide's
   own: the tile is 165px wide and the panel is up to 700px, so the tile's 10px
   reads as a glyph stuck to its words at this size. */
.lb-slide.lb-unfetched.video .lb-unfetched-note {
  flex-direction: column; gap: 16px;
}
/* The spinner would never stop here: the slide holds no image, so offer.js's
   `wireLoadState` (scoped to `picture img`) never adds `lb-loaded`. Same
   suppression, and for the same reason, as `.lb-video` above — and the same
   specificity trick: the base rule is (0,3,0), so the override has to carry
   both `:not()`s to reach (0,4,0). */
.lb-slide.lb-unfetched:not(.lb-loaded):not(.lb-error)::after { content: none; }
/* The slide's flex item is the <picture> (00094's AVIF slot), but an
   unsized picture has auto height, so the img's max-height:100% resolved
   against nothing and portrait images overflowed (clipped top+bottom).
   display:contents dissolves the picture box: the img itself is the flex
   item again and its percentages resolve against the slide. */
.lb-slide picture { display: contents; }
/* Scoped to `picture img` on purpose: a bare `.lb-slide img` would also
   match the .lb-ph placeholder and out-specific its 100% sizing. */
.lb-slide picture img {
  max-width: 100%; max-height: 100%;
  width: auto; height: auto; object-fit: contain;
  display: block;
  cursor: default;
  /* Above the placeholder while it is still fading underneath. */
  position: relative; z-index: 1;
}

.lb-close, .lb-nav {
  position: absolute; z-index: 2;
  border: none; cursor: pointer; color: #fff;
  background: rgba(0, 0, 0, 0.4); border-radius: 999px;
  display: flex; align-items: center; justify-content: center;
  line-height: 1;
  /* Flex centers the line box, but ×/‹/› ink sits below its midpoint
     (baseline metrics), reading as low-shifted. Zero the UA button
     padding, then nudge the optical center up. */
  padding: 0 0 4px;
}
.lb-close {
  top: 12px; right: 12px; width: 40px; height: 40px; font-size: 26px;
}
.lb-nav {
  top: 50%; transform: translateY(-50%);
  width: 46px; height: 46px; font-size: 30px;
  padding: 0 0 5px;
}
.lb-prev { left: 12px; }
.lb-next { right: 12px; }
.lb-nav:disabled { opacity: 0.25; cursor: default; }
.lb-close:hover, .lb-nav:not(:disabled):hover { background: rgba(0, 0, 0, 0.65); }

/* Desktop gallery row below the stage: the thumb variants, current marked.
   In flow (not absolute) so the column layout reserves its height and the
   stage image stops exactly at the gallery's top edge. */
.lb-gallery {
  position: relative; z-index: 2; flex: none;
  margin: 0 0 12px;
  display: flex; gap: 8px; max-width: 92vw;
  padding: 8px; overflow-x: auto;
  background: rgba(0, 0, 0, 0.35); border-radius: 12px;
}
.lb-thumb {
  flex: none; padding: 0; border: 2px solid transparent; border-radius: 8px;
  background: none; cursor: pointer; line-height: 0;
}
.lb-thumb img {
  width: 72px; height: 54px; object-fit: cover;
  border-radius: 6px; display: block; opacity: 0.6;
}
/* The missing photos' rail thumb (owner decision, 2026-08-01): the same slot
   the photo thumbs take, with the hatch instead of a picture — the rail ends
   level, and the reader can reach the last slide from the rail like any other.
   It holds no `img`, so the slot has to be sized on the button itself.

   The size is 76×58, not 72×54 (bug rl-jkv9). A photo thumb is a 72×54 `img`
   INSIDE the 2px border above, so its outer box is 76×58. `* { box-sizing:
   border-box }` at the top of this file makes the `width`/`height` here the
   OUTER box, so 72×54 left this thumb 4px short of every photo thumb beside
   it. Size this rule alone: every branch of the placeholder — photos only,
   photos with a failed video — takes it, so there is no second rule to agree
   with.

   The border is the thumb's own current-marker, so it starts transparent and
   `.lb-thumb.is-current` below (same specificity, later in the file) turns it
   white exactly as it does for a photo thumb. The shared surface rule above
   gives the box a 1px `--line` border, which would otherwise read as a mark
   this thumb has not earned. */
.lb-thumb.lb-thumb-unfetched {
  width: 76px; height: 58px;
  border-width: 2px; border-color: transparent; border-radius: 8px;
  opacity: 0.6;
}
/* The same thumb where the video pass failed (issues rl-wfeh and rl-lf12): the
   hatched thumb above, plus the play glyph. It takes its box from the rule
   above.

   The glyph is centred in the slot and takes the page ink, not the video
   thumb's white-on-black circle: that circle sits over a poster IMAGE, and here
   there is only the hatch under it. */
.lb-thumb.lb-thumb-unfetched.video {
  display: flex; align-items: center; justify-content: center;
  color: var(--ink-soft);
}
.lb-thumb.lb-thumb-unfetched.video .lb-thumb-play {
  display: flex; width: 22px; height: 22px;
}
.lb-thumb.lb-thumb-unfetched.video .lb-thumb-play svg { width: 100%; height: 100%; }
.lb-thumb.is-current { border-color: #fff; }
.lb-thumb.lb-thumb-unfetched.is-current { opacity: 1; }
.lb-thumb.is-current img { opacity: 1; }

/* The video's gallery-row thumb (issue 00122): same 72×54 slot, poster
   image, with a small play glyph so it reads as video among the photo
   thumbs — `.lb-thumb-video` is a `button.lb-thumb`, so it inherits the
   base sizing/current-state rules above; this only adds the glyph. */
.lb-thumb-video { position: relative; }
.lb-thumb-video .lb-thumb-play {
  /* 2× (user request 2026-07-24) — 18px vanished on the thumb. */
  position: absolute; inset: 0; margin: auto; width: 36px; height: 36px;
  display: flex; align-items: center; justify-content: center;
  color: #fff; background: rgba(0, 0, 0, 0.45); border-radius: 999px;
  padding: 6px;
}
.lb-thumb-video .lb-thumb-play svg { width: 100%; height: 100%; }

/* Touch: no gallery row (pointer:coarse OR narrow viewport), swipe instead.
   Both guards so a coarse pointer on any width and a narrow screen on any
   pointer both drop the row. */
@media (pointer: coarse), (max-width: 640px) {
  .lb-gallery { display: none; }
  /* Arrows are keyboard/desktop affordances; swipe drives touch. */
  .lb-nav { display: none; }
}

/* «Детали» — the extraction payload's factual structure (issue 00083),
   in the compare template's vocabulary: fact grid, fee rows, chips. */
.details { margin-bottom: 16px; }
/* The detail page's three section headers — «Детали», «Примерное расположение»
   and «Оригинал объявления». One rule, three selectors: they were three
   identical copies, and a copy is where the next size change goes missing.
   `.details` keeps its child selector so the fact labels inside it (`.fact .k`,
   11px) stay field labels rather than becoming headers. */
.details > .k, .mapbox .k, .raw .k {
  font-size: 15px; color: var(--ink-faint); text-transform: uppercase;
  letter-spacing: 0.04em; margin: 22px 0 8px;
}
.details .facts { margin-bottom: 0; }
.grp {
  font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--ink-faint); margin: 12px 0 6px;
}
.fees { border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
.fee {
  display: flex; justify-content: space-between; gap: 12px; align-items: baseline;
  padding: 9px 14px; border-bottom: 1px solid var(--line);
  font-size: 13.5px; background: var(--card);
}
.fee:last-child { border-bottom: none; }
.fee .lbl { overflow-wrap: anywhere; }
.fee .lbl .note { color: var(--warn); font-size: 11.5px; }
.fee .val {
  font-variant-numeric: tabular-nums; text-align: right;
  overflow-wrap: anywhere;
}
.chips { display: flex; gap: 6px; flex-wrap: wrap; }
.chip {
  font-size: 12px; padding: 2px 9px; border-radius: 999px;
  background: var(--chip); color: var(--ink-soft); font-weight: 600;
}
.chip.gate { background: #f7e3e3; color: #a33; }
@media (prefers-color-scheme: dark) {
  .chip.gate { background: #3a2020; color: #e58585; }
}
.chip.cchip { font-weight: 500; }
.chip.cchip .ck {
  opacity: 0.55; font-weight: 700; text-transform: uppercase;
  font-size: 10px; letter-spacing: 0.04em; margin-right: 5px;
}
.chip.cchip a {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
}

.mapbox { margin-bottom: 16px; }
/* Square, not a 280px band. A square holds the same ground north-south as
   east-west, so the accent circle sits in context instead of clipped top and
   bottom. The height follows the column (max 680px), so it needs no media
   query. Leaflet reads the size once at init: aspect-ratio resolves from the
   width, which is known before the script runs. */
#offer-map {
  aspect-ratio: 1 / 1; border: 1px solid var(--line); border-radius: 10px;
  background: repeating-linear-gradient(45deg, var(--hatch1), var(--hatch1) 12px, var(--hatch2) 12px, var(--hatch2) 24px);
}
.mapbox .note { font-size: 11.5px; color: var(--ink-faint); margin-top: 5px; }
/* Leaflet's control chrome is hardcoded white; theme it with the page. */
@media (prefers-color-scheme: dark) {
  /* We bake ONE light tile set (00128; a dark bake is its own project), so
     dark mode re-colours the tiles instead of downloading a second set.
     The filter is CSS, so the browser re-applies it on an OS theme change
     with no reload — the standing requirement in MANUAL-QA.

     The filter flips lightness, then rotates the hues back. Lightness has
     to flip. We shipped brightness(0.72) first, and a dimmed light map is
     still a grey light map that glares against --bg. Dark mode is not a
     darker white. What invert(1) alone breaks is hue — it sends the sea
     orange. hue-rotate puts the hues back, so water reads as water and
     parks as parks while land goes dark and roads read as light lines.

     We use 203deg rather than the customary 180deg. CSS hue-rotate is a
     linear matrix approximation, not a true rotation, so 180deg lands the
     sea slightly off. We chose all five numbers against real tiles in
     deploy/tiles/lab/darkmode.html. Change them there, not here.

     The pane holds tiles alone — the accent circle and dot live in the
     overlay pane and stay at full strength. */
  #offer-map .leaflet-tile-pane {
    filter: invert(1) hue-rotate(203deg) brightness(1.2) contrast(0.85) saturate(1.1);
  }
  #offer-map .leaflet-control-zoom a {
    background: var(--card); color: var(--ink);
    border-color: var(--line);
  }
  #offer-map .leaflet-control-zoom a.leaflet-disabled {
    background: var(--surface); color: var(--ink-faint);
  }
  #offer-map .leaflet-bar { border-color: var(--line); }
  #offer-map .leaflet-control-attribution {
    background: rgba(30, 28, 25, 0.8); color: var(--ink-soft);
  }
  #offer-map .leaflet-control-attribution a { color: var(--accent); }
}

/* No padding-top: the header carries the gap (`.details > .k` and friends), and
   a border-top stops that margin collapsing, so padding here would stack on top
   of it and set this header further from its rule than the other two. */
.raw { margin-top: 16px; border-top: 1px solid var(--line); }
.raw .post-text {
  margin: 0; white-space: pre-wrap; word-wrap: break-word;
  font: 12.5px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  color: var(--ink-soft); background: var(--surface); border: 1px solid var(--line);
  border-radius: 8px; padding: 10px 12px;
}

/* The Lister's own formatting inside «Оригинал объявления» (issue 00203).
   `pre` and `blockquote` arrive as <code class="pre"> and <span class="quote">,
   because a run split by an overlapping span would otherwise nest a block
   element inside a <b>. */
.raw .post-text code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.95em; }
.raw .post-text code.pre { display: inline-block; max-width: 100%; }
.raw .post-text .quote {
  border-left: 2px solid var(--line); padding-left: 8px; color: var(--ink-faint);
}
/* A spoiler gets no rule at all: this block exists so a reader can check the
   Lister's own words, and Telegram's tap-to-reveal would put a step in front
   of exactly that. `domain::tg_message` renders the span as plain text. */
/* A `text_url`'s target, spelled out after the text that hides it. */
.raw .post-text .link-target { color: var(--ink-faint); word-break: break-all; }

/* One vendored custom emoji (issue 00192 vendored it, 00203 draws it).
   The picture is an <img>; the text beside it is what a copy carries, shrunk
   to nothing rather than hidden — `display: none` and `visibility: hidden`
   both drop a node out of the clipboard, and the point of the text is the
   clipboard. Verified in a browser: docs/MANUAL-QA.md. */
.raw .post-text .tg-emoji-art {
  width: 1.25em; height: 1.25em; object-fit: contain; vertical-align: -0.22em;
}
.raw .post-text .tg-emoji-copy { font-size: 0; }

/* The attribution, now inside «Оригинал объявления» above the quote it names.
   14.5px rather than 12.5px: this line is the answer to "who wrote this and
   when", and at the old size a reader skimming the quote box slid past it. */
.source { font-size: 14.5px; color: var(--ink-faint); margin: 0 0 8px; }
.source a { color: var(--accent); }
