/* ============================================================
   VIBES SHEET — page-type components
   Loads after vibes.css. Same tokens, no new ones except
   the verdict ramp, which is semantic and must survive theming.
   ============================================================ */

/* ---------- BREADCRUMB ---------- */
/* The title block's nav treatment, applied to the generated pages'
   breadcrumb: hairline underlines at the sheet's own scale, not the
   coloured link row it inherited from style.css. Supersedes the three
   .seo-breadcrumb rules still sitting in vibes.css. */

.seo-breadcrumb {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px;
  font-size: 14px; color: var(--muted); margin: 0 0 24px;
}
.seo-breadcrumb a {
  display: inline-flex; align-items: center; min-height: 44px; color: var(--ink);
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--rule) 26%, transparent);
  text-decoration-thickness: 1px; text-underline-offset: 4px;
}
.seo-breadcrumb a:hover { color: var(--signal); }
.seo-breadcrumb__sep { color: var(--faint); }

/* ---------- VERDICT (BUY / WAIT / SELL) ---------- */
/* Semantic, so it does NOT follow --signal. It has to read the
   same on both domains or the meaning shifts between sites. */

:root {
  --verdict-buy:   #3D6B2E;
  --verdict-wait:  #8A5E0E;
  --verdict-sell:  #A83A19;
}
@media (prefers-color-scheme: dark) {
  :root { --verdict-buy: #8CBB6E; --verdict-wait: #E0AA45; --verdict-sell: #E0703F; }
}

.verdict {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  border: 2px solid currentColor;
  padding: 4px 12px;
  font-family: var(--font-data);
  font-size: 14px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.verdict--buy  { color: var(--verdict-buy); }
.verdict--wait { color: var(--verdict-wait); }
.verdict--sell { color: var(--verdict-sell); }
.verdict small { font-size: 11px; letter-spacing: 0.06em; opacity: 0.85; }

/* The verdict is a reading of public data, not advice. Always
   pair it with .verdict-basis in the markup — never let it stand alone. */
.verdict-basis { font-size: 14px; color: var(--muted); margin: 8px 0 0; }

/* The same reading at row scale, for .list__item asides, ledger cells and
   ranked rows. Colour and a hairline underline, never a pill — the coloured
   background it had came from style.css:3871 and _base.html's inline block,
   and both leave in 5.4 / 8.5. Without this rule every signal_badge() call
   on the site renders as unstyled body text, and 8.5's sweep cannot see it. */
.signal-badge {
  font-family: var(--font-data);
  font-size: 12px;
  font-weight: 400;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  border-bottom: 2px solid currentColor;
  padding: 0 0 1px;
  border-radius: 0;
}
.signal-badge.signal-buy  { color: var(--verdict-buy);  background: none; }
.signal-badge.signal-wait { color: var(--verdict-wait); background: none; }
.signal-badge.signal-sell { color: var(--verdict-sell); background: none; }

/* ---------- SCORE BAR (vibe score, confidence) ---------- */
/* A surveyor's scale bar, not a donut. Position on a line is the
   most accurately read encoding there is. */

.score { margin: 0 0 4px; }
.score__head { display: flex; justify-content: space-between; align-items: baseline; font-size: 14px; }
.score__val { font-family: var(--font-data); font-size: 15px; color: var(--signal); }
.score__track { position: relative; height: 26px; border-bottom: var(--hair); margin-top: 4px; }
.score__track::before,
.score__track::after {
  content: ""; position: absolute; bottom: 0; width: 1px; height: 7px; background: var(--rule); opacity: 0.5;
}
.score__track::before { left: 0; }
.score__track::after  { right: 0; }
.score__tick {
  position: absolute; bottom: 0; width: 2px; height: 20px; background: var(--signal);
  transform: translateX(-1px);
}
.score__scale { display: flex; justify-content: space-between; font-family: var(--font-data); font-size: 11px; color: var(--faint); margin-top: 4px; }

/* ---------- FACT GRID (replaces the four stat boxes) ---------- */

.facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); border-top: var(--hair); border-left: var(--hair); }
.facts__cell { border-right: var(--hair); border-bottom: var(--hair); padding: 12px 16px; }
.facts__label { font-size: 12px; letter-spacing: 0.07em; text-transform: uppercase; color: var(--muted); margin: 0 0 4px; }
.facts__value { font-family: var(--font-display); font-variation-settings: "wdth" 108; font-weight: 700; font-size: 26px; line-height: 1.1; margin: 0; }
.facts__note  { font-size: 13px; color: var(--muted); margin: 3px 0 0; }
.facts__value--up   { color: var(--verdict-buy); }
.facts__value--down { color: var(--verdict-sell); }

/* ---------- CHAPTER NAV (Vibe / Money / Living / Risks) ---------- */
/* Sticky, hairline, no pills. */

/* The full-bleed margin/padding pair assumed a container with 24px of side
   padding to cancel against. Its parent is an unpadded <main>, so it bled 24px
   past both edges and every one of the 132 suburb pages scrolled the document
   sideways at every width — 1464 against 1440, 414 against 390. The internal
   overflow-x stays: it is how four chapter links fit at 390px. */
/* The rule sits edge to edge, the links do not. Without the max-width the tabs
   started at the viewport's 24px while every other element on the page started
   at the 1180px content column, which read as the nav having slipped left. */
.chapters {
  position: sticky; top: 0; z-index: 20;
  background: var(--paper);
  border-top: var(--hair); border-bottom: var(--hair);
}
.chapters__inner {
  display: flex; gap: 0; overflow-x: auto;
  width: 100%; max-width: var(--sheet-max); margin: 0 auto;
  padding: 0 24px;
  scrollbar-width: none;
}
.chapters__inner::-webkit-scrollbar { display: none; }
.chapters::-webkit-scrollbar { display: none; }
.chapters a {
  flex: none; padding: 12px 16px 11px; text-decoration: none;
  font-size: 14px; white-space: nowrap;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.chapters a[aria-current="true"] { border-bottom-color: var(--signal); color: var(--signal); }

/* ---------- MAP ---------- */

.map {
  position: relative;
  aspect-ratio: 4 / 3;
  border: var(--hair);
  background: var(--paper-2);
}
@media (min-width: 900px) { .map { aspect-ratio: 16 / 9; } }

.map__canvas { position: absolute; inset: 0; }

/* MapLibre chrome, de-branded to hairlines */
.maplibregl-ctrl-group { border-radius: 0 !important; box-shadow: none !important; border: var(--hair) !important; background: var(--paper) !important; }
.maplibregl-ctrl-group button { border-radius: 0 !important; }
.maplibregl-ctrl-attrib { background: color-mix(in srgb, var(--paper) 85%, transparent) !important; font-family: var(--font-data); font-size: 11px; }
.maplibregl-popup-content {
  border-radius: 0; box-shadow: none; border: var(--hair);
  background: var(--paper); color: var(--ink);
  padding: 12px 12px; font-family: var(--font-body); font-size: 14px;
}
.maplibregl-popup-tip { display: none; }
.popup__dist { font-family: var(--font-data); font-size: 13px; color: var(--signal); }
.popup__title { font-weight: 500; margin: 2px 0 0; }
.popup__meta { font-size: 13px; color: var(--muted); margin: 4px 0 0; }

/* Map legend sits under the map, never floating over it */
.map-legend { display: flex; flex-wrap: wrap; gap: 8px 24px; padding: 12px 0; border-bottom: var(--hair); font-size: 14px; }
.map-legend span { display: inline-flex; align-items: center; gap: 8px; }
/* No `background` here. `.map-legend i` is 0-1-1 and every .mk-* modifier below
   is 0-1-0, so a base colour on this rule outranks all of them and painted the
   whole legend --signal red while the clip-paths (uncontested) still gave the
   right shapes. Shape correct, colour uniformly wrong, which is the confusing
   half-failure. Each .mk-* now supplies its own colour and nothing overrides
   it. Kept in sync with SHAPES in js/map.js, which is what drawMapLegend()
   renders once a report exists — this markup is the pre-search fallback. */
.map-legend i { width: 11px; height: 11px; flex: none; font-style: normal; }
/* Shape carries meaning as well as colour — never colour alone. */
.mk-approval      { background: var(--signal); }                                   /* square   */
.mk-contamination { background: var(--verdict-sell); clip-path: polygon(50% 0,100% 100%,0 100%); } /* triangle */
.mk-venue         { background: var(--signal-2); border-radius: 50%; }              /* circle   */
.mk-noise         { background: var(--signal-2); clip-path: polygon(50% 0,100% 50%,50% 100%,0 50%); } /* diamond */
.mk-hazard        { background: var(--muted); clip-path: polygon(50% 0,100% 38%,82% 100%,18% 100%,0 38%); }
.mk-origin        { background: var(--ink); }                                      /* the address itself */
.mk-road          { background: var(--muted); clip-path: polygon(0 34%,100% 34%,100% 66%,0 66%); }  /* bar */
.mk-works         { background: var(--signal); transform: scale(0.72); }            /* small square */
/* The only open shape on the map: a sale is evidence about a market, not a
   thing that acts on you. Ring, not disc — and a ring here too, because the
   swatch has to be the shape the map actually draws. */
.mk-sale          { background: none; border: 2px solid var(--verdict-buy); border-radius: 50%; }

/* ---------- ARTICLE (guides, insights, stories) ---------- */

.article { max-width: 720px; margin: 0 auto; padding: 0 24px; }
.article__head { padding: 48px 0 24px; border-bottom: var(--hair); }
.article__kicker { font-family: var(--font-data); font-size: 12px; letter-spacing: 0.09em; text-transform: uppercase; color: var(--signal); margin: 0 0 12px; }
.article__meta { display: flex; flex-wrap: wrap; gap: 4px 16px; font-family: var(--font-data); font-size: 12px; color: var(--muted); margin: 16px 0 0; }
.article__body { padding: 32px 0; font-size: 17px; line-height: 1.68; }
.article__body > * { max-width: 100%; }
.article__body p  { margin: 0 0 16px; }
.article__body ul, .article__body ol { margin: 0 0 16px; padding-left: 24px; }
.article__body li { margin-bottom: 8px; }
.article__body blockquote { margin: 24px 0; padding-left: 16px; border-left: 2px solid var(--signal); font-size: 19px; line-height: 1.4; }
.article__body a { text-decoration-color: color-mix(in srgb, currentColor 40%, transparent); }

/* Contents — an index to a set of notes, so numbering is honest */
.contents { border-top: var(--hair); border-bottom: var(--hair); padding: 16px 0; margin: 0 0 32px; }
.contents h2 { font-size: 12px !important; font-family: var(--font-data); font-weight: 400; letter-spacing: 0.09em; text-transform: uppercase; color: var(--muted); margin: 0 0 8px; }
.contents ol { list-style: none; margin: 0; padding: 0; counter-reset: c; }
.contents li { counter-increment: c; display: grid; grid-template-columns: 34px 1fr; padding: 4px 0; font-size: 15px; }
.contents li::before { content: counter(c, decimal-leading-zero); font-family: var(--font-data); font-size: 12px; color: var(--faint); padding-top: 3px; }
.contents a { text-decoration: none; border-bottom: var(--hair-soft); }

/* ---------- CARD LIST (insights index, rankings, neighbours) ---------- */
/* Rows, not cards. Dense lists read better ruled than boxed. */

.list { border-top: var(--hair); }
.list__item { display: grid; grid-template-columns: 1fr auto; gap: 12px 16px; padding: 16px 0; border-bottom: var(--hair-soft); align-items: baseline; }
.list__item h3 { margin: 0 0 4px; }
.list__item h3 a { text-decoration: none; }
.list__item p { margin: 0; font-size: 15px; color: var(--muted); max-width: 66ch; }
.list__aside { font-family: var(--font-data); font-size: 13px; color: var(--muted); text-align: right; white-space: nowrap; }
@media (max-width: 620px) {
  .list__item { grid-template-columns: 1fr; }
  /* nowrap keeps "$1.3M · +11.6% YoY" on one line while the aside is its own
     column. Once the row stacks there is no column to protect, and an aside
     carrying prose rather than numbers — the project stage on
     /major-projects/, 60+ characters — held the grid open to 624px and pushed
     the whole page sideways at 390. */
  .list__aside { text-align: left; white-space: normal; }
}

/* ---------- SOURCE LINE ---------- */
/* Every data section ends with one. This is the credibility engine
   of the whole design — never omit it. */

.source {
  display: flex; flex-wrap: wrap; gap: 4px 12px;
  font-family: var(--font-data); font-size: 12px; color: var(--faint);
  padding-top: 8px; margin-top: 16px; border-top: var(--hair-soft);
}
.source a { color: inherit; }

/* ============================================================
   EXPLORATION MODE
   For the reader who has no address yet — moving up from Sydney,
   knows the city, knows no suburb. The data pages assume knowledge
   this person doesn't have. These three components are the way in.
   ============================================================ */

/* ---------- SECOND DOOR ---------- */
/* Sits directly under the address field on the homepage. One line.
   Half the traffic can't answer "what's your address?" — this is
   where they go instead. */

/* Above the fold it is a framed block, not a footnote. §2b called for one line,
   but a line under a search field is what the orienting reader's eye skips —
   and that reader is the half of the traffic who cannot answer the question the
   hero just asked. Framed in hairlines, no fill, no radius: it earns attention
   from weight and position, not decoration. The one-line form survives as
   .door--inline for the footer register. */
.door {
  border: var(--hair); margin-top: 24px; padding: 24px;
}
.door__q {
  font-family: var(--font-display); font-variation-settings: "wdth" var(--wdth-display);
  font-weight: 700; font-size: 22px; line-height: 1.15; margin: 0 0 8px; max-width: 26ch;
}
.door__a { font-size: 16px; color: var(--muted); margin: 0 0 16px; max-width: 48ch; }
.door__link {
  display: inline-flex; align-items: center; min-height: 44px;
  font-family: var(--font-data); font-size: 13px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--signal); text-decoration: none;
  border-bottom: 2px solid var(--signal);
}
.door__link:hover { color: var(--ink); border-bottom-color: var(--ink); }

/* The footer repeat. One line, as §2b asked — a second framed block there would
   compete with the one above the fold instead of supporting it. */
.door--inline {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px;
  border: 0; border-top: var(--hair-soft); margin-top: 16px; padding: 12px 0 0;
  font-size: 15px;
}
.door--inline span { color: var(--muted); }
.door--inline a { text-decoration: none; border-bottom: 1px solid var(--signal); color: var(--signal); padding-bottom: 1px; }

/* ---------- LENSES ---------- */
/* Ways into 130 suburbs for someone who can't name one. Real links to
   real pages — no JS filtering, this is a static site. Each lens is a
   question a mover actually asks, not a data facet. */

.lenses { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); border-top: var(--hair); border-left: var(--hair); }
.lens {
  border-right: var(--hair); border-bottom: var(--hair);
  padding: 16px 16px 16px; text-decoration: none; display: block;
}
.lens:hover { background: var(--paper-2); }
.lens h3 { margin: 0 0 4px; }
.lens p { margin: 0; font-size: 14px; color: var(--muted); max-width: 34ch; }
.lens__count { font-family: var(--font-data); font-size: 12px; color: var(--faint); display: block; margin-top: 8px; }

/* ---------- PLATE ---------- */
/* The only images in the system. Hairline frame, mono caption naming
   the place and the date it was taken. Documentary, not marketing.
   NEVER stock photography — it destroys the one thing this design buys.
   Suburb and walk pages only. Never on a data page. */

.plate { margin: 0 0 24px; }
.plate img { display: block; width: 100%; height: auto; border: var(--hair); }
.plate figcaption {
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: 4px 16px;
  font-family: var(--font-data); font-size: 12px; color: var(--muted);
  padding-top: 8px;
}
.plate--wide { grid-column: 1 / -1; }

/* A strip of three, for a suburb's character section */
.plates { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin: 0 0 24px; }
.plates .plate { margin: 0; }
.plates figcaption { display: block; }

/* ---------- CHARACTER ---------- */
/* The plain-language read on a place. On the current site this is the
   warmest thing on the page and it sits buried under fifteen data
   sections. It belongs at the top. */

.character { font-size: 19px; line-height: 1.55; max-width: 46ch; margin: 0 0 24px; }
.character strong { font-weight: 500; }

/* The ruled box needs air above it: with margin 0 its top hairline sat flush
   against the last line of the trajectory paragraph, so "Best for" read as a
   continuation of the prose rather than a separate answer to a separate
   question. */
.fit { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 0; border-top: var(--hair); border-left: var(--hair); margin: var(--s6) 0 0; }
.fit__cell { border-right: var(--hair); border-bottom: var(--hair); padding: 16px 16px; }
.fit__cell h4 { font-size: 12px; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); font-weight: 500; margin: 0 0 8px; }
.fit__cell ul { list-style: none; margin: 0; padding: 0; font-size: 15px; }
.fit__cell li { padding: 3px 0 3px 16px; position: relative; }
.fit__cell li::before { content: ""; position: absolute; left: 0; top: 11px; width: 7px; height: 1px; background: var(--muted); }

/* ---------- SUBURB LIST ROW ---------- */
/* The suburbs index is an orientation surface, not a directory.
   Character sentence first, numbers second. */

.suburb-row { display: grid; grid-template-columns: 1fr auto; gap: 8px 24px; padding: 16px 0; border-bottom: var(--hair-soft); align-items: baseline; }
.suburb-row h3 { margin: 0 0 4px; }
.suburb-row h3 a { text-decoration: none; }
.suburb-row p { margin: 0; font-size: 15px; color: var(--muted); max-width: 60ch; }
.suburb-row__nums { font-family: var(--font-data); font-size: 13px; color: var(--muted); text-align: right; white-space: nowrap; }
.suburb-row__nums b { display: block; font-weight: 400; color: var(--ink); font-size: 15px; }
@media (max-width: 620px) {
  .suburb-row { grid-template-columns: 1fr; }
  .suburb-row__nums { text-align: left; }
}

/* ---------- NOTES / FAQ ---------- */
/* Every FAQ on the site, sitewide (PAGE-TYPES §1). A disclosure list on
   hairlines — the rules that styled it lived in _base.html's inline block
   with a coloured summary and a +/− glyph, and left with it in 5.4.
   The markup is faq_section() in _components.html and is not 5.4's to change,
   so the class names stay and take the .notes treatment. */

.faq-section { border-top: var(--hair); margin-top: 48px; }
.faq-section > h2 {
  font-family: var(--font-data); font-size: 12px; font-weight: 400;
  letter-spacing: 0.09em; text-transform: uppercase; color: var(--muted);
  margin: 16px 0 0;
}
.faq-item { border-bottom: var(--hair-soft); }
.faq-item summary {
  padding: 16px 0; cursor: pointer; font-size: 17px; list-style: none;
  display: grid; grid-template-columns: 1fr 20px; gap: 16px; align-items: baseline;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
  content: "+"; font-family: var(--font-data); font-size: 15px;
  color: var(--muted); text-align: right;
}
.faq-item[open] summary::after { content: "\2212"; }
.faq-item summary:hover { color: var(--signal); }
.faq-answer { padding: 0 0 16px; }
.faq-answer p { margin: 0; font-size: 16px; color: var(--muted); max-width: 66ch; }

/* ---------- SUBURB PAGE SCAFFOLDING ---------- */
/* The suburb template's content moved onto the primitives in phase 3, but its
   structural wrappers — .container for width, .section for vertical rhythm —
   were left on style.css, which 5.4 removes. Without these two rules all 132
   suburb pages render full-bleed to the viewport edge.
   They are the .sheet primitive under the names the markup already uses.
   The markup should become .sheet / .block when a task next owns suburb.html;
   107 further classes on that page are still unstyled — see BLOCKED.md. */

.container { width: 100%; max-width: var(--sheet-max); margin: 0 auto; padding: 0 24px; }
.section   { padding: 32px 0; }
.section:first-child { padding-top: 16px; }

/* ---------- SUBURB PAGE: STRUCTURAL RULES style.css USED TO CARRY ---------- */
/* Task 5.4 removed style.css and I recorded the 107 orphaned classes as
   "mostly redundant wrappers … the visual loss is small". That was wrong about
   one of them: .suburb-map carried `height: 400px`, and without it MapLibre
   initialised into a zero-height box on all 132 suburb pages — canvas present,
   markers present, no tiles ever requested, nothing visible. The map looked
   deleted. These are the rules that do structural work, in the sheet
   vocabulary: hairlines, no radius, no shadow. */

.map-container-suburb { position: relative; width: 100%; }
.suburb-map { height: 420px; width: 100%; border: var(--hair); overflow: hidden; }
@media (max-width: 620px) { .suburb-map { height: 300px; } }

.chapter { position: relative; }
/* The badge is a <p>, so it inherited the global 16px paragraph bottom margin
   and sat in the row with phantom space under it — which is what read as the
   momentum box being misaligned against the heading. A badge is not a
   paragraph; it carries no margin of its own here. */
/* Centred, not baseline-aligned. Baseline is right when two runs of text share
   a line; the badge is a bordered box about 26px tall standing next to a 54px
   heading, so hanging it off the heading's baseline parked it in the lower
   third of the word. With line-height 1.05 the line-box centre and the cap
   height's optical centre are within half a pixel of each other, so centring
   puts the badge level with the word itself. */
.suburb-title-row { display: flex; align-items: center; gap: var(--s4); flex-wrap: wrap; }
.suburb-title-row .verdict { margin: 0; }

/* The momentum grid. Its only rules lived in style.css, which this page does
   not load, so it butted straight against the block above it. */
.hero-intelligence:not(:empty) { margin-top: var(--s5); }
.suburb-actions { display: flex; gap: 16px; flex-wrap: wrap; }
.suburb-da-list { display: flex; flex-direction: column; }
.da-item { display: grid; grid-template-columns: 1fr auto; gap: 4px 16px; align-items: baseline; padding: 12px 0; border-bottom: var(--hair-soft); }
.da-item-desc { margin: 0; }
.da-item-meta { font-family: var(--font-data); font-size: 13px; color: var(--muted); white-space: nowrap; }
.spillover-card { padding: 12px 0; border-bottom: var(--hair-soft); }
.spillover-card-header { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; }
.spillover-card-body { display: flex; align-items: baseline; gap: 12px; font-size: 15px; color: var(--muted); }
.section-title { font-family: var(--font-display); font-variation-settings: "wdth" 106; font-weight: 700; }
.section-subtitle { color: var(--muted); font-size: 15px; margin-top: 4px; }

/* ============================================================
   ADDRESS RESULTS (/check.html)
   The commercial page, and the last consumer of style.css. Its
   report is rendered by js/app.js, which emits ~370 class names
   across 50 families — renaming them all would be a rewrite of
   the paid deliverable, so the page's SKELETON moves onto the
   primitives (title block, .field, .facts, .map, .rows, .notes)
   and the interior detail is restyled here in the same
   vocabulary: hairlines, no radius, no shadow, mono for numbers.
   Everything below is scoped under .report-section or an id, so
   none of it can leak onto another page type.
   ============================================================ */

/* ---------- report shell ---------- */
.report-section { padding-bottom: 32px; }
.report-header { padding: 32px 0 16px; border-bottom: var(--hair); }
.report-address { font-family: var(--font-display); font-variation-settings: "wdth" 106; font-weight: 700; font-size: clamp(26px, 4vw, 40px); line-height: 1.08; margin: 0; }
.report-framing, .report-generated, .report-trust-line { font-family: var(--font-data); font-size: 12px; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); margin: 8px 0 0; }
/* What the 500 m radius is measured from. Sentence case, not uppercase — it is
   a caveat to read, not a label to scan. */
.report-precision { font-family: var(--font-data); font-size: 12px; line-height: 1.5; color: var(--muted); max-width: 60ch; margin: 8px 0 0; }
.report-precision.hidden { display: none; }
.report-suburb-link { display: inline-block; margin-top: 8px; font-size: 15px; }

/* Each report section is a ruled block, not a card */
.report-section-card { border-bottom: var(--hair); padding: 0; margin: 0; background: none; }
.section-header { display: flex; align-items: baseline; gap: 12px; padding: 24px 0 12px; cursor: pointer; }
.section-title { font-family: var(--font-display); font-variation-settings: "wdth" 106; font-weight: 700; font-size: 22px; margin: 0; display: flex; align-items: baseline; gap: 8px; }
.section-count { font-family: var(--font-data); font-size: 13px; color: var(--muted); font-weight: 400; }
.section-subtitle { font-size: 15px; color: var(--muted); margin: 0 0 12px; }
.section-content { padding-bottom: 24px; }
.section-content.collapsed { display: none; }
.collapse-icon { margin-left: auto; font-family: var(--font-data); font-size: 13px; color: var(--faint); }
.section-alert, .section-recommendation { border-left: 2px solid var(--signal); padding: 8px 0 8px 12px; margin: 12px 0; font-size: 15px; }

/* ---------- findings: the distance-row primitive ---------- */
.item-card, .gated-headline-item, .related-da-card, .hazard-card, .safety-card, .outlook-card, .livability-grade-card, .alert-body {
  display: grid; grid-template-columns: 1fr auto; gap: 4px 16px;
  align-items: baseline; padding: 12px 0; border-bottom: var(--hair-soft);
  background: none; border-radius: 0; box-shadow: none;
}
.item-header, .gated-headline-content, .hazard-header, .safety-header, .outlook-header { display: contents; }
.item-name, .hazard-title, .gated-headline-content strong { font-weight: 500; font-size: 16px; }
.item-distance, .item-badge, .hazard-severity, .gated-headline-meta, .item-details, .item-description, .related-da-meta {
  font-family: var(--font-data); font-size: 13px; color: var(--muted);
}
.item-icon, .hazard-icon, .section-icon, .livability-icon, .traffic-icon, .railway-icon,
.outlook-icon, .timeline-icon, .gated-clear-icon, .hazards-clear-icon, .subsection-icon,
.show-more-icon, .btn-icon, .signal-icon, .cat-icon { display: none; }

/* ---------- stat rows: the fact grid ---------- */
.gated-stat-row, .trajectory-display, .safety-categories, .pulse-categories, .crime-intel-strip {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  border-top: var(--hair); border-left: var(--hair); margin: 12px 0;
}
.gated-stat, .trajectory-metric, .safety-category, .crime-intel-chip, .pulse-cat-chip {
  border-right: var(--hair); border-bottom: var(--hair); padding: 12px 16px; background: none; border-radius: 0;
}
.gated-stat-value, .trajectory-value, .score-value, .chip-number, .hero-number {
  display: block; font-family: var(--font-display); font-variation-settings: "wdth" 108;
  font-weight: 700; font-size: 24px; line-height: 1.1;
}
.gated-stat-label, .trajectory-label, .score-label, .chip-label, .cat-label {
  display: block; font-size: 12px; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); margin-bottom: 3px;
}

/* ---------- paywall: a ruled row, never a card ---------- */
.gated-more, .gated-report-link, .da-overflow, .show-more-label {
  font-size: 15px; color: var(--muted); margin: 12px 0 0;
}
.gated-clear, .hazards-clear { display: block; font-size: 15px; color: var(--muted); padding: 12px 0; }
.gated-summary { margin: 0; }

/* ---------- the offer ---------- */
.offer-section, .offer-card { border-top: var(--hair); padding: 32px 0 0; margin-top: 32px; background: none; }
.offer-header h3, .offer-header h2 { font-family: var(--font-display); font-variation-settings: "wdth" 106; font-weight: 700; }
.offer-tiers, .offer-features { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); border-top: var(--hair); border-left: var(--hair); margin: 16px 0; }
.offer-tier { border-right: var(--hair); border-bottom: var(--hair); padding: 16px 16px; background: none; border-radius: 0; }
.offer-btn, .report-sticky-cta-btn, .check-btn, .alert-action-btn, .modal-action-btn {
  border: 0; background: var(--ink); color: var(--paper); border-radius: 0;
  padding: 12px 24px; font-family: var(--font-data); font-size: 13px;
  letter-spacing: .08em; text-transform: uppercase; cursor: pointer; text-decoration: none; display: inline-block;
}
.offer-btn:hover, .report-sticky-cta-btn:hover, .check-btn:hover, .alert-action-btn:hover, .modal-action-btn:hover { background: var(--signal); }
.offer-btn--professional { background: var(--signal); }
.offer-data-line, .offer-anchor-text, .offer-intro-badge { font-family: var(--font-data); font-size: 12px; color: var(--muted); }

/* ---------- sticky purchase bar ---------- */
.report-sticky-cta {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  background: var(--paper); border-top: var(--hair); padding: 12px 24px;
}
.report-sticky-cta.hidden { display: none; }
/* The bar is fixed, so it sits over whatever the page ends with — on a phone
   that was the copyright line. Reserve its height at the foot of the document
   while it is on screen; js/app.js toggles .has-sticky-cta alongside it.
   Composed from the bar's own padding plus a scale step rather than a single
   magic number, so the reserve still clears the bar if the label wraps. */
body.has-sticky-cta { padding-bottom: calc(var(--s3) * 2 + var(--s7)); }

/* ---------- modal ---------- */
/* Visibility is driven by .active (js/app.js:3107), NOT by .hidden. Defaulting
   this to display:flex left the item modal open over the address field on every
   load — caught by screenshotting the page, not by reading the diff. */
.modal { position: fixed; inset: 0; z-index: 60; display: none; align-items: center; justify-content: center; padding: 24px; }
.modal.active { display: flex; }
.modal-backdrop { position: absolute; inset: 0; background: color-mix(in srgb, var(--ink) 55%, transparent); }
.modal-content { position: relative; background: var(--paper); border: var(--hair); border-radius: 0; box-shadow: none; max-width: 560px; width: 100%; max-height: 86vh; overflow-y: auto; padding: 24px; }
.modal-header { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; border-bottom: var(--hair); padding-bottom: 12px; margin-bottom: 16px; }
.modal-close {
  border: 0; background: none; font-size: 21px; line-height: 1;
  cursor: pointer; color: var(--muted);
  min-width: 44px; min-height: 44px;  /* touch target, not just a glyph */
}
.modal-actions { display: flex; flex-wrap: wrap; gap: 12px; border-top: var(--hair); padding-top: 16px; margin-top: 16px; }

/* ---------- everything else the report renders ---------- */
.story-block, .crime-intel, .livability-item, .traffic-roads-summary, .pulse-bar-row,
.timeline-event, .developer-info, .insight-details, .vibe-list, .data-sources-summary,
.data-disclaimer-card, .hazards-advice, .signal-item, .alert-header, .subsection-title {
  background: none; border-radius: 0; box-shadow: none;
}
.story-heading, .subsection-title, .alert-header { font-weight: 500; font-size: 16px; margin: 16px 0 4px; }
.story-text, .story-context, .livability-detail, .traffic-text, .railway-text, .hazard-description,
.crime-intel-tip, .pulse-coverage-note, .hazards-clear-note, .data-disclaimer-card, .verdict-disclaimer {
  font-size: 15px; color: var(--muted); max-width: 66ch;
}
.story-signal-positive { color: var(--verdict-buy); }
.story-signal-negative { color: var(--verdict-sell); }
.trust-badge, .status-chip, .signal-tag, .hazard-detail-chip, .traffic-road-tag, .vibe-badge, .pulse-badge {
  font-family: var(--font-data); font-size: 12px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted); border-radius: 0; background: none;
}
.trust-dot, .verdict-dot { display: none; }
.pulse-bar-track { height: 3px; background: var(--paper-2); border-radius: 0; }
.pulse-bar-fill { height: 3px; background: var(--signal); border-radius: 0; }
.spinner { display: none; }
.no-data { color: var(--faint); font-size: 15px; }

@media (max-width: 620px) {
  .item-card, .gated-headline-item, .related-da-card, .hazard-card, .safety-card,
  .outlook-card, .livability-grade-card, .alert-body { grid-template-columns: 1fr; }
  .report-sticky-cta { padding: 8px 16px; flex-wrap: wrap; }
}

/* ---------- BACK / SECONDARY LINK ---------- */
/* .btn.btn-secondary survives on the suburb page's footer link and was one of
   the 107 classes orphaned when 5.4 removed style.css. It is a link, not a
   button: hairline underline, ink, and a 44px target. */
.btn, .back-link {
  display: inline-flex; align-items: center; min-height: 44px;
  font-size: 15px; color: var(--ink);
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--rule) 26%, transparent);
  text-decoration-thickness: 1px; text-underline-offset: 4px;
}
.btn:hover, .back-link:hover { color: var(--signal); }

/* ── Locator ─────────────────────────────────────────────────────────────── */
/* Where a suburb is, for a reader who has never been to Newcastle. Set in the
   data face and ruled top and bottom, so it reads as measurement rather than
   as another sentence competing with the character copy above it. */
.locator {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--s3);
  margin: var(--s3) 0 0;
  padding: var(--s2) 0;
  border-top: var(--hair-soft);
  border-bottom: var(--hair-soft);
  font-family: var(--font-data);
  font-size: 12px;
  line-height: 1.5;
  color: var(--muted);
}

/* Trailing separator, not leading. With `span + span::before` the dot wraps to
   the START of each new line, which reads as a bullet list that has lost its
   indent. Trailing leaves the break clean. */
.locator span:not(:last-child)::after {
  content: " ·";
  margin-left: var(--s1);
  color: var(--faint);
}

/* ── Region note ─────────────────────────────────────────────────────────── */
/* What Newcastle or Lake Macquarie actually is, once per page, for the reader
   who arrived from a search and has never been here. Inset against the page
   rule rather than boxed: it is context, not a callout. */
.region-note {
  margin: var(--s5) 0 0;
  padding-left: var(--s4);
  border-left: 2px solid var(--signal);
}

.region-note__label {
  margin: 0 0 var(--s1);
  font-family: var(--font-data);
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

.region-note p { margin: 0 0 var(--s2); }

.region-note__more {
  margin: 0;
  font-size: 14px;
}

/* ==========================================================================
   REPORT COMPONENTS RECOVERED FROM style.css
   --------------------------------------------------------------------------
   check.html loads vibes.css + vibes-components.css only. 76 classes used by
   the rendered report were still styled exclusively in css/style.css, which
   no longer ships on this page — so the offer block, the verdict, the map
   frame, the signal bar and every collapsible section header rendered with
   browser defaults. The section headers are <button> elements, which is why
   they showed as shrink-to-fit boxes with a 2px outset border and text
   sitting on the frame.

   Restyled here in the vibes idiom rather than ported from style.css: ruled
   blocks, hairlines, no cards, no radii, no shadows, and spacing from the
   4-based scale that tests/test_design_consistency.py enforces.
   ========================================================================== */

/* ---------- collapsible section headers ---------- */
/* A <button> that must read as a heading row, not a control. */
.collapsible-trigger {
  width: 100%;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  -webkit-appearance: none;
  appearance: none;
}
.collapsible-trigger:focus-visible {
  outline: 2px solid var(--signal);
  outline-offset: 2px;
}
/* The collapse indicator sits at the far right of the row, on the rule. */
.collapsible-trigger .collapse-icon { margin-left: auto; padding-left: var(--s3); }
.collapsible-trigger[aria-expanded="false"] .collapse-icon { transform: none; }

/* ---------- verdict ---------- */
.verdict-section {
  border-top: var(--hair);
  border-bottom: var(--hair);
  padding: var(--s5) 0;
  margin: var(--s5) 0;
}
.verdict-header {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s3); flex-wrap: wrap;
}
.verdict-title-row { display: flex; align-items: baseline; gap: var(--s2); }
.verdict-title {
  font-family: var(--font-display);
  font-variation-settings: "wdth" 106;
  font-weight: 700; font-size: 22px; margin: 0;
}
/* No .verdict-dot rule here on purpose: the sheet hides .trust-dot and
   .verdict-dot outright (line ~527). Meaning is carried by the word, not a
   coloured bead. The verdict tone rides on the title instead. */
.verdict-section.verdict-caution .verdict-title { color: var(--verdict-wait); }
.verdict-section.verdict-proceed .verdict-title { color: var(--verdict-buy); }
.verdict-section.verdict-concern .verdict-title { color: var(--verdict-sell); }
.verdict-signal-badge {
  font-family: var(--font-data); font-size: 12px; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--muted);
  border: var(--hair); padding: var(--s1) var(--s2);
}
.verdict-signal-badge.badge-caution { color: var(--verdict-wait); }
.verdict-explanation { font-size: 17px; margin: var(--s3) 0 0; }
.verdict-synthesis { font-size: 15px; color: var(--muted); margin: var(--s2) 0 0; }
.verdict-highlights { margin-top: var(--s4); }
.highlights-label {
  font-family: var(--font-data); font-size: 12px; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--muted); margin-bottom: var(--s2);
}
.highlights-list, .items-list { list-style: none; margin: 0; padding: 0; }
.highlights-list li, .items-list li {
  padding: var(--s2) 0; border-bottom: var(--hair-soft); font-size: 15px;
}
.highlights-list li:last-child, .items-list li:last-child { border-bottom: 0; }
.verdict-disclaimer, .disclaimer-content {
  font-size: 13px; color: var(--muted); margin: var(--s4) 0 0;
}
.disclaimer-warning { color: var(--signal); }

/* ---------- suburb signal bar ---------- */
.suburb-signal-bar {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--s2) var(--s4);
  border-bottom: var(--hair); padding-bottom: var(--s3); margin-bottom: var(--s4);
}
.signal-bar-badge {
  font-family: var(--font-data); font-size: 12px; letter-spacing: 0.08em;
  padding: var(--s1) var(--s2); border: 1px solid currentColor;
}
.signal-bar-badge.signal-buy  { color: var(--verdict-buy); }
.signal-bar-badge.signal-wait { color: var(--verdict-wait); }
.signal-bar-badge.signal-sell { color: var(--verdict-sell); }
.signal-bar-chip {
  font-family: var(--font-data); font-size: 12px; color: var(--muted);
}
/* Separation comes from the flex gap, not a ::before bullet — the bullet
   belongs to the chip that follows it, so on a phone the bar wrapped and left
   a stray "·" leading the second line. */
.signal-bar-phase { color: var(--signal); }
.signal-bar-link { margin-left: auto; font-size: 14px; }

/* ---------- map ---------- */
.map-section { margin: var(--s5) 0; }
.map-header { margin-bottom: var(--s3); }
.map-container { border: var(--hair); }
.map-note {
  font-family: var(--font-data); font-size: 13px; color: var(--muted);
  margin: var(--s3) 0 0;
}
.map-note-link { color: var(--signal); }

/* ---------- gated teasers ---------- */
.gated-trajectory-display { margin: var(--s3) 0; }
.gated-trajectory-signal { display: flex; align-items: baseline; gap: var(--s2); }
.gated-trajectory-label {
  font-family: var(--font-data); font-size: 12px; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--muted);
}
.gated-trajectory-value { font-size: 17px; font-weight: 600; }
.gated-trajectory-signal.rising    .gated-trajectory-value { color: var(--verdict-buy); }
.gated-trajectory-signal.declining .gated-trajectory-value { color: var(--verdict-sell); }
.gated-stat-warn { color: var(--signal); }
.gated-type-breakdown {
  font-family: var(--font-data); font-size: 13px; color: var(--muted);
  margin-top: var(--s2);
}

/* ---------- safety + crime ---------- */
.safety-verdict p { margin: 0 0 var(--s2); }
.safety-rating, .rating-badge {
  font-family: var(--font-data); font-size: 12px; letter-spacing: 0.06em;
  text-transform: uppercase;
}
.rating-elevated, .hazard-high, .vs-high, .chip-red, .callout-danger { color: var(--verdict-sell); }
.chip-amber, .badge-caution, .warning, .cat-declining { color: var(--verdict-wait); }
.good, .cat-rising { color: var(--verdict-buy); }
.momentum-neutral, .safety-momentum { color: var(--muted); }
.crime-intel-hero { margin: var(--s3) 0; }
.crime-intel-callout {
  font-size: 15px; margin-top: var(--s3);
  border-left: 2px solid var(--signal); padding-left: var(--s3);
}
.cat-rate { font-size: 15px; }
.cat-vs-state { font-family: var(--font-data); font-size: 12px; color: var(--muted); }
.hazard-specifics, .hazards-verify {
  font-size: 14px; color: var(--muted); margin-top: var(--s2);
}

/* ---------- offer ---------- */
.offer-positioning {
  font-family: var(--font-data); font-size: 12px; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--signal); margin: 0 0 var(--s2);
}
.offer-title { font-size: 26px; margin: 0 0 var(--s3); }
.offer-lead { font-size: 16px; color: var(--muted); margin: 0; max-width: 62ch; }
.offer-tier-header { margin-bottom: var(--s2); }
.offer-tier-name {
  font-family: var(--font-display); font-variation-settings: "wdth" 106;
  font-weight: 700; font-size: 17px; margin: 0 0 var(--s1);
}
.offer-price { display: flex; align-items: baseline; gap: var(--s2); }
.offer-price-original {
  font-family: var(--font-data); font-size: 14px; color: var(--faint);
  text-decoration: line-through;
}
.offer-price-current {
  font-family: var(--font-display); font-weight: 700; font-size: 26px;
}
.offer-tier--professional .offer-price-current { color: var(--signal); }
.offer-tier-tagline { font-size: 14px; color: var(--muted); margin: var(--s2) 0 var(--s3); }
.offer-features { display: block; border: 0; margin: 0 0 var(--s4); }
.offer-features li {
  padding: var(--s2) 0; border-bottom: var(--hair-soft); font-size: 14px;
}
.offer-features li:last-child { border-bottom: 0; }
.offer-trust, .offer-intro-note {
  font-family: var(--font-data); font-size: 12px; color: var(--muted);
  margin-top: var(--s3);
}
.offer-sources-link { color: var(--signal); }
/* Not an empty scroll target despite the name — it carries the "you'll spend
   $600 on a building inspection" argument. Collapsing it to height:0 spilled
   that paragraph over the block beneath it. */
.offer-anchor { margin-top: var(--s4); }
.offer-anchor-text { font-size: 14px; color: var(--muted); margin: 0; max-width: 62ch; }

/* ---------- footer of the report ---------- */
.data-sources-freshness {
  font-family: var(--font-data); font-size: 12px; color: var(--muted);
  margin-top: var(--s4);
}
.new-search { margin: var(--s6) 0 0; }
.new-search-btn {
  font-family: var(--font-data); font-size: 13px; letter-spacing: 0.08em;
  text-transform: uppercase; cursor: pointer;
  background: none; color: var(--ink);
  border: var(--hair); padding: var(--s3) var(--s4);
}
.new-search-btn:hover { background: var(--ink); color: var(--paper); }
.suburb-link-name { font-weight: 600; }
.suburb-link-arrow { font-family: var(--font-data); font-size: 13px; color: var(--muted); }
/* inline-flex with the default align-items:stretch let the small arrow ride up
   over the underline of the suburb name. */
.report-suburb-link { align-items: baseline; gap: var(--s2); }
/* .trust-dot is display:none by design, so without a layout the three badges
   ran together as one sentence. */
.report-trust-line { display: flex; flex-wrap: wrap; gap: var(--s1) var(--s4); }

@media (max-width: 620px) {
  .verdict-header { align-items: flex-start; }
  .signal-bar-link { margin-left: 0; width: 100%; }
  .offer-title { font-size: 22px; }
  .offer-price-current { font-size: 24px; }
}

/* Similar suburbs, relocated out of the character chapter to sit with the
   other ways off the page (template comment explains why). Empty until
   renderCompareList() fills it, so it must not reserve space when unused. */
.compare-block-section:has(#compare-block:empty) { display: none; }
.compare-block-section .section-subtitle { margin-bottom: var(--s4); }

/* Cache note: the ?v= stamp is md5 of vibes.css + this file (generate_pages.py
   :2553), and worker.js stores versioned assets in caches.default for a week.
   If a request lands while a deploy is mid-flight, the edge can store the OLD
   body under the NEW hash and serve it for the full TTL — which is what
   happened to the .fit margin above on 2026-08-07. Symptom: dist/ has the
   change, the live page references the right ?v=, and the served CSS does not
   match. Fix without a cache-purge token: change a byte here so the hash moves. */

/* ---------- safety block ---------- */
/* .safety-card was borrowing the .item-card grid, which is `1fr auto` — built
   for a one-line row with a distance on the right. Safety is not that shape:
   the verdict is one line, the crime panel beside it is six, and the category
   figures below relate to the crime panel, not to the verdict. The result was
   a 170px hole under a 34px sentence, with the numbers that explain the panel
   sitting in a different column from it. Stack it: verdict, then the panel,
   then the figures that explain the panel, all on one left edge. */
.safety-card { display: block; padding: 0; border-bottom: 0; }
.safety-header { display: block; }
.safety-verdict { margin-bottom: var(--s4); }
.crime-intel { margin-bottom: var(--s4); }
.safety-categories { margin-top: 0; }

/* The cross-site door at the foot of a suburb list. In plain .suburb-row markup
   it was indistinguishable from the 93 suburbs above it, and when it sat at the
   top of the Lake Mac list it read as that list's heading. A rule above it and
   the site's own accent make it obviously a way out rather than another entry. */
.suburb-cross {
  border-top: var(--hair);
  margin-top: var(--s5);
  padding-top: var(--s4);
}
.suburb-cross h3 { font-size: 15px; }
.suburb-cross a { color: var(--signal); }

/* The one editorial route from a suburb's risk data into the page that sells
   the address-level version of it. /report/ had no in-body inbound links at
   all — every link to it on the site was one of four entries in the footer. */
.risk-block-more { font-size: 14px; color: var(--muted); margin: var(--s2) 0 0; }
.risk-block-more a { color: var(--signal); }
.cta-box-more { font-size: 14px; margin: var(--s3) 0 0; }
.cta-box-more a { color: var(--signal); }
