
  /* Surface ladder + hairlines carry depth; there are no shadows anywhere on this page.
     Type is an 8-step integer scale, spacing is strictly 4px multiples, and the accent
     is reserved for brand, primary action and focus. Semantic colour is status only. */
  :root{
    --canvas:#010102; --s1:#0f1011; --s2:#141516; --s3:#18191a;
    --hair:#23252a; --hair-strong:#34343a;

    /* Text. --ink-faint was #62666d and failed AA across ~50 elements, including the
       bylaw citations, which are the most credibility-bearing text on the page. */
    --ink:#f7f8f8; --ink-muted:#d0d6e0; --ink-subtle:#9aa1ab; --ink-faint:#7f8792;

    --accent:#5e6ad2; --accent-hi:#828fff; --accent-ring:rgba(94,106,210,.30);

    /* Status. Verdict words are luminance-matched so a stop reads as emphatically as a go. */
    --good:#5ccf9a; --warn:#e9b357; --bad:#f2686c;
    --good-tint:rgba(92,207,154,.10); --good-line:rgba(92,207,154,.28);
    --warn-tint:rgba(233,179,87,.10); --warn-line:rgba(233,179,87,.28);
    --bad-tint:rgba(242,104,108,.10);  --bad-line:rgba(242,104,108,.30);
    --good-ink:#8fdcb8; --warn-ink:#f0cb90; --bad-ink:#f7a5a7;
    --stop-word:#ff9ea2;   /* luminance-matched to go/caution for the verdict word only */

    /* Type: 12 13 14 16 19 23 29 35 -- integers only, no half-pixels.
       Every step moved up one. The old ramp put body copy at 13px and every caption, chip,
       citation and chart label at 11px, which is where most of the words on the page live: the
       screen read as a wall of fine print, and the fine print is the part carrying the sources.
       11px is a footnote size and this page has no footnotes -- its provenance IS the content. */
    --f1:12px; --f2:13px; --f3:14px; --f4:16px; --f5:19px; --f6:23px; --f7:29px; --f8:35px;
    /* Spacing: 4px multiples only. */
    --sp1:4px; --sp2:8px; --sp3:12px; --sp4:16px; --sp5:20px; --sp6:24px; --sp7:32px; --sp8:40px;
    --r-xs:4px; --r-sm:6px; --r-md:8px; --r-lg:12px;
    --measure:680px;                        /* controls stop stretching to the full width */
  }
  *{box-sizing:border-box}
  html{background:var(--canvas)}
  body{
    margin:0; background:var(--canvas); color:var(--ink-muted);
    font:400 var(--f3)/1.5 "Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;
    font-feature-settings:"cv01","ss03";
    -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
  }
  .wrap{max-width:1320px;margin:0 auto;padding:var(--sp8) var(--sp6) 96px}
  ::selection{background:rgba(94,106,210,.35);color:var(--ink)}

  header{margin-bottom:var(--sp5)}
  .city-nav{display:flex;flex-wrap:wrap;gap:var(--sp2);margin-bottom:var(--sp6)}
  .city-nav a{border:1px solid var(--hair);border-radius:999px;padding:.3rem .7rem;color:var(--ink-subtle);font-size:var(--f2);text-decoration:none}
  .city-nav a[aria-current="page"]{color:var(--ink);border-color:var(--accent);font-weight:600}
  .city-nav a:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
  header .brand{margin:0;font-size:var(--f3);font-weight:500;letter-spacing:.3px;color:var(--ink-subtle)}
  header .brand::before{content:"";display:inline-block;width:9px;height:9px;margin-right:var(--sp2);
                        border-radius:2px;background:var(--accent);vertical-align:1px}
  header p{margin:var(--sp3) 0 0;max-width:60ch;font-size:var(--f4);line-height:1.55;color:var(--ink-subtle)}

  /* ---------- search: capped, not stretched ---------- */
  .search{position:relative;max-width:var(--measure)}
  .search-row{display:flex;gap:var(--sp2)}
  #addr{flex:1;min-height:40px;padding:var(--sp2) var(--sp3);font:inherit;font-size:var(--f3);
        color:var(--ink);background:var(--s1);border:1px solid var(--hair);
        border-radius:var(--r-md);transition:border-color .12s ease}
  #addr::placeholder{color:var(--ink-faint)}
  #addr:hover{border-color:var(--hair-strong)}
  #addr:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-ring)}
  #go{min-height:40px;padding:0 var(--sp4);font:inherit;font-size:var(--f3);font-weight:500;
      color:#fff;background:var(--accent);border:1px solid transparent;border-radius:var(--r-md);
      cursor:pointer;white-space:nowrap;transition:background .12s ease}
  #go:hover{background:var(--accent-hi)}
  #go:focus-visible{outline:none;box-shadow:0 0 0 3px var(--accent-ring)}

  #ac{position:absolute;left:0;right:0;top:44px;z-index:30;display:none;overflow:hidden;
      background:var(--s3);border:1px solid var(--hair-strong);border-radius:var(--r-md)}
  #ac div{display:flex;justify-content:space-between;align-items:center;gap:var(--sp3);
          min-height:40px;padding:var(--sp2) var(--sp3);font-size:var(--f3);
          color:var(--ink-muted);cursor:pointer}
  #ac div:hover{background:rgba(255,255,255,.05);color:var(--ink)}
  #ac div[aria-selected="true"]{background:rgba(94,106,210,.20);color:var(--ink)}
  #ac .p{font-size:var(--f2);color:var(--ink-faint);white-space:nowrap}

  .chips{display:flex;flex-wrap:wrap;gap:var(--sp2);margin-top:var(--sp3);align-items:center}
  .chips .lbl{font-size:var(--f2);color:var(--ink-subtle)}
  /* Chips wrap in pairs (see where SAMPLES is appended): one line when all fit, otherwise two
     balanced ones -- never three chips and an orphan. Narrow screens let the pair itself wrap
     rather than overflow. */
  .chip-pair{display:flex;gap:var(--sp2);flex-wrap:wrap}
  .crosslink{margin:var(--sp4) 0 0;max-width:var(--measure);font-size:var(--f2);
             line-height:1.55;color:var(--ink-subtle)}
  .crosslink a{color:var(--accent-hi)}
  .chip{min-height:32px;padding:var(--sp2) var(--sp3);font:inherit;font-size:var(--f2);
        color:var(--ink-subtle);background:transparent;border:1px solid var(--hair);
        border-radius:999px;cursor:pointer;transition:all .12s ease;
        /* R5 iteration 2 (HIGH finding 1): the DTD-assembly chip below is a real <a>, not a
           <button> like every other chip -- without this it inherits the browser's default
           underline, which none of the button-based chips carry. */
        text-decoration:none;display:inline-flex;align-items:center}
  .chip:hover{color:var(--ink);border-color:var(--hair-strong);background:var(--s1)}

  .note{margin-top:var(--sp5);min-height:20px;font-size:var(--f3);color:var(--ink-subtle)}
  .note.err{max-width:var(--measure);padding:var(--sp3);color:var(--bad-ink);
            background:var(--bad-tint);border:1px solid var(--bad-line);border-radius:var(--r-md)}

  /* A READ TAKING LONGER THAN THESE READS NORMALLY TAKE SAYS SO, AND SAYS HOW LONG.
     Amber, the page's existing vocabulary for "this is about our read, not a verdict about the
     land" (.flag, .cf-note.precondition, .verdict.read) -- never the red of .note.err, which
     would read as a failure while the read is still out and might still answer. The explicit
     display:none is not optional: [hidden] loses to display:flex, so without it the block would
     sit on the page empty on every healthy lookup. */
  .lookup-wait{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--sp3);
               max-width:var(--measure);margin-top:var(--sp3);padding:var(--sp3);
               background:var(--warn-tint);border:1px solid var(--warn-line);
               border-radius:var(--r-md)}
  .lookup-wait[hidden]{display:none}
  .lookup-wait-elapsed{font-variant-numeric:tabular-nums;font-weight:600;font-size:var(--f4);
                       color:var(--warn-ink)}
  .lookup-wait-says{flex:1 1 240px;min-width:0;margin:0;font-size:var(--f2);line-height:1.5;
                    color:var(--warn-ink)}

  .empty{margin-top:var(--sp6);max-width:var(--measure);padding:var(--sp6);background:var(--s1);
         border:1px solid var(--hair);border-radius:var(--r-lg)}
  .empty h2{margin:0 0 var(--sp4);font-size:var(--f4);font-weight:600;color:var(--ink)}
  .empty ol{margin:0;padding-left:var(--sp5);color:var(--ink-subtle);font-size:var(--f3)}
  .empty li{margin:var(--sp2) 0}

  @keyframes sh{0%,100%{opacity:.45}50%{opacity:.8}}
  .sk{background:var(--s2);border-radius:var(--r-sm);animation:sh 1.4s ease-in-out infinite}

  /* ---------- verdict ---------- */
  .verdict{margin-top:var(--sp6);padding:var(--sp6);background:var(--s1);
           border:1px solid var(--hair);border-radius:var(--r-lg);position:relative;overflow:hidden}
  .verdict::before{content:"";position:absolute;inset:0 0 auto 0;height:2px;background:var(--hair-strong)}
  .verdict.go{border-color:var(--good-line);background:linear-gradient(180deg,var(--good-tint),transparent 60%),var(--s1)}
  .verdict.go::before{background:var(--good)}
  .verdict.caution{border-color:var(--warn-line);background:linear-gradient(180deg,var(--warn-tint),transparent 60%),var(--s1)}
  .verdict.caution::before{background:var(--warn)}
  .verdict.stop{border-color:var(--bad-line);background:linear-gradient(180deg,var(--bad-tint),transparent 60%),var(--s1)}
  .verdict.stop::before{background:var(--bad)}
  /* A refusal that is about OUR COVERAGE rather than about the land takes no alarm ink at all:
     nothing is wrong with the parcel and nothing is wrong with the tool. */
  .verdict.coverage{border-color:var(--hair-strong);background:var(--s1)}
  .verdict.coverage::before{background:var(--hair-strong)}
  .call.coverage .word{color:var(--ink)}
  /* A read that did not answer is UNSETTLED, and says so in the one channel a reader takes in
     before any words: the rule at the top of the card is broken rather than solid, so it does
     not look like the finished, settled refusal beside it. */
  .verdict.read{border-color:var(--warn-line);background:linear-gradient(180deg,var(--warn-tint),transparent 60%),var(--s1)}
  .verdict.read::before{background:repeating-linear-gradient(90deg,var(--warn) 0 8px,transparent 8px 14px)}
  .call.read .word{color:var(--warn)}
  /* One short line saying which KIND of statement the headline under it is. It replaces four
     restatements of the headline itself (the pinned bar, the source-coverage line, a refusal-
     reason paragraph and the coverage chip), every one of which said that sentence again. */
  .verdict-eyebrow{margin:0 0 var(--sp2);font-size:var(--f2);font-weight:600;letter-spacing:.07em;
                   text-transform:uppercase;color:var(--ink-subtle)}
  .verdict.read .verdict-eyebrow{color:var(--warn-ink)}
  /* The retry belongs to the transient family only. Offering it on a settled refusal would
     invite a reader to re-run a result that cannot change. */
  .refusal-retry{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp3);margin-top:var(--sp3)}
  .refusal-retry .sub{flex:1 1 260px;min-width:0;margin:0;font-size:var(--f2);color:var(--ink-subtle)}
  .call{display:flex;align-items:baseline;gap:var(--sp3);flex-wrap:wrap}
  .call .word{font-size:var(--f7);font-weight:600;letter-spacing:-.6px;line-height:1.15;color:var(--ink)}
  .call.go .word{color:var(--good)} .call.caution .word{color:var(--warn)} .call.stop .word{color:var(--stop-word)}
  .call .addr{font-size:var(--f3);color:var(--ink-subtle)}
  .verdict .vbody{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,.85fr);
                  gap:var(--sp6);margin-top:var(--sp4);align-items:start}
  .verdict.solo .vbody{grid-template-columns:minmax(0,1fr)}
  .verdict p{max-width:78ch}
  @media(max-width:920px){.verdict .vbody{grid-template-columns:minmax(0,1fr);gap:var(--sp4)}}
  .verdict p{margin:0;font-size:var(--f4);line-height:1.6;color:var(--ink-subtle)}
  .verdict p b{color:var(--ink);font-weight:500}
  .flag{padding:var(--sp3);font-size:var(--f3);line-height:1.5;border-radius:var(--r-md);
        background:var(--warn-tint);border:1px solid var(--warn-line);color:var(--warn-ink)}
  .flag + .flag{margin-top:var(--sp2)}
  .flag b{color:var(--ink)}
  .flag.block{background:var(--bad-tint);border-color:var(--bad-line);color:var(--bad-ink)}

  /* A RECORDED ANSWER IS NOT A FINDING ABOUT THE SITE, SO IT DOES NOT LOOK LIKE ONE.
     It rides the flag list because that is the card's one always-visible, price-preserving
     slot -- the same one the land-tenure caveat rides -- but it takes a heavier left rule and
     a named label, so a reader scanning the card meets "RECORDED ANSWER" before reading a word
     of it. Amber, the page's existing vocabulary for a statement about our read rather than a
     verdict about the land; never red, because nothing is wrong with the answer, only with how
     current it is, and the date beside it is what says how current. */
  .flag.recorded{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--sp2) var(--sp3);
                 border-left:4px solid var(--warn)}
  .recorded-label{font-size:var(--f1);font-weight:700;letter-spacing:.07em;
                  text-transform:uppercase;color:var(--warn-ink);white-space:nowrap}
  .recorded-says{flex:1 1 220px;min-width:0}

  /* R3-adjacent-parcel-assembly: a candidate card carries a full occupancy sentence plus a
     button, which does not fit .strip/.cell's fixed 3-row subgrid (built for a short
     label/value/caption triple) -- reusing it overlapped the occupancy text with the button
     underneath it. Plain flex cards, own class, no subgrid. */
  .assembly-candidates{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));
    gap:var(--sp3);margin-top:var(--sp3)}
  .assembly-card{display:flex;flex-direction:column;padding:var(--sp3);border:1px solid var(--hair);
    border-radius:var(--r-md);background:var(--s1)}
  .assembly-card .k{font-size:var(--f2);font-weight:600}
  .assembly-card .v{margin-top:var(--sp1);font-size:var(--f2);color:var(--ink-subtle)}
  .assembly-card p.sub{flex:1}
  .assembly-card button{margin-top:var(--sp3);align-self:flex-start}

  /* ---------- metrics ---------- */
  /* overflow MUST stay visible here: the six headline numbers carry the same
     [data-help] tooltips as everything else, and clipping the strip silently
     swallowed all six - the one line in the header telling people to hover was
     pointing at the only numbers that couldn't. Corner rounding moves to the end
     cells so the strip still reads as one rounded object. */
  /* SUBGRID, so the three rows line up because they ARE the same rows.
     Label, value and caption used to align via a min-height on the label: a floor, not an
     alignment mechanism. It cleared one address and the next one broke it -- a tile whose
     label carries a provenance chip needs 36px against the 32.4px guess, so its value dropped
     3.6px below the rest, and any longer label does the same. Sharing the strip's own rows
     means a tall label makes the whole ROW taller and nothing slips. */
  .strip{display:grid;grid-template-columns:1.35fr 1.35fr repeat(4,minmax(0,1fr));
         grid-template-rows:auto auto auto;
         margin-top:var(--sp3);background:var(--s1);border:1px solid var(--hair);
         border-radius:var(--r-lg);overflow:visible}
  /* The verdict strip carries a seventh tile (FAR) that the no-density strip does not; the
     class is set by renderNumbers, so a six-cell strip keeps the six-column template. */
  .strip.with-far{grid-template-columns:1.35fr 1.35fr repeat(5,minmax(0,1fr))}
  .strip .cell:first-child{border-radius:var(--r-lg) 0 0 var(--r-lg)}
  .strip .cell:last-child{border-radius:0 var(--r-lg) var(--r-lg) 0}
  .cell{display:grid;grid-template-rows:subgrid;grid-row:span 3;align-content:start;
        padding:var(--sp4);border-left:1px solid var(--hair)}
  /* Where subgrid is unavailable, the old flex column and the min-height floor. Imperfect for
     the reason above, and better than a heap. */
  @supports not (grid-template-rows: subgrid){
    .cell{display:flex;flex-direction:column}
    .cell .k{min-height:2.7em}
  }
  .cell:first-child{border-left:none}
  .cell.hero{background:var(--s2)}
  /* R5-honest-uncertainty: the residual moved out of the hero cell (AC1) into a later cell
     carrying its own ".ceiling" class, so the tone tint below follows the number whose sign
     it actually reflects, not whichever cell happens to be biggest on screen. The hero cell
     (now a zoning-capacity figure -- floor area) is a measured fact and is never tinted by
     the verdict tone. */
  .cell.ceiling{background:var(--s2)}
  .strip.go .cell.ceiling{background:linear-gradient(180deg,var(--good-tint),transparent),var(--s2)}
  .strip.caution .cell.ceiling{background:linear-gradient(180deg,var(--warn-tint),transparent),var(--s2)}
  .strip.stop .cell.ceiling{background:linear-gradient(180deg,var(--bad-tint),transparent),var(--s2)}
  .cell .k{font-size:var(--f1);font-weight:500;letter-spacing:.4px;color:var(--ink-subtle)}
  /* THREE sizes, and which number gets which is a claim about confidence, not about
     importance. Floor area is the hero because it is DERIVED FROM THE BYLAW: measured lot,
     cited setbacks, cited ratio. The ceiling is second and a step down because it is the most
     assumption-dependent figure on the page -- it moves with revenue, cost, margin and
     absorption, none of which this screen can source. R5 spent a whole ring demoting it out
     of the hero slot for exactly that reason, and a design pass briefly put it back before
     the test that ring left behind caught it.
     What the design pass got RIGHT and is kept: the ceiling was sixth of six, below the fold
     of the eye, which reads as an afterthought rather than as a caveated headline. It is now
     second, larger than the supporting tiles, and never as large as the measured figure. */
  .cell .v{margin-top:var(--sp2);font-size:var(--f5);font-weight:500;
           line-height:1.15;letter-spacing:-.4px;color:var(--ink);font-variant-numeric:tabular-nums}
  .cell.hero .v{font-size:var(--f8);font-weight:600;letter-spacing:-1px}
  .cell.ceiling:not(.hero) .v{font-size:var(--f6);font-weight:600;letter-spacing:-.6px}
  /* Chips sit in a fixed slot on the label line, so their left edges form a column the eye can
     run down. They were previously embedded mid-caption at whatever x the text happened to
     wrap to, which is why provenance could not be scanned without reading. */
  .cell .k{display:flex;align-items:center;gap:var(--sp2);justify-content:space-between}
  .cell .s{margin-top:var(--sp1);font-size:var(--f1);color:var(--ink-faint)}
  /* Two lines before truncating, not one: single-line ellipsis cut every longer zone name at
     the tile's width ("Medium Density Apartme..."), and most Kelowna zone descriptions fit
     comfortably on two. The clamp keeps a genuinely long one from growing the row, and the
     title attribute still carries the full name. */
  /* max-height and align-self:start are load-bearing, not decoration. The strip's subgrid
     STRETCHES this caption to the row height the hero's own tall caption sets, so the box
     never overflows and a line-clamp alone never fires -- measured live: the description
     rendered every line, ellipsis and all, at 210px tall. Pinning the box to two lines
     (3em at line-height 1.5) makes the overflow real; the clamp then draws the ellipsis. */
  .cell .s.clip{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
                overflow:hidden;line-height:1.5;max-height:3em;align-self:start}
  /* A source action and its explanation must remain fully visible and focusable.
     Keep the description-only clamp, but let source-bearing captions grow. */
  .cell .s.clip:has(>.provenance-tap){display:block;-webkit-line-clamp:unset;
    overflow:visible;max-height:none}
  /* R1-highest-and-best: which scheme the hero number prices sits directly under it, in the
     same weight as the rest of the caption -- not a footnote, since AC3 requires a reader not
     have to infer this. The operator's own scheme, when different, is a second, visually
     quieter line so the two are never confused for each other. */
  .cell.hero.ceiling .s .hero-scheme{color:var(--ink-subtle);font-weight:500}
  /* The sweep's own answer, one clear step down from the figure the card is about: its own
     label, its own figure at var(--f5) against the primary's var(--f7), and its own sentence.
     Quieter by SIZE and by placement, never by a dimmer that reads as fine print -- it is a real
     figure a reader may want to act on, and the Levers card still carries the button that adopts
     it. */
  /* The prose column flexes; the figure column is sized for its figure. `auto 1fr` was the
     other way round: the label took its full max-content (286px of a 342px row at 400px) and
     left the 1fr column its min-content, which is what put `$352K` and the Source button into
     a 44px column. The label is words and can wrap; the figure and the button cannot. */
  .cell.hero.ceiling .s .hero-best-scheme{display:grid;grid-template-columns:minmax(0,1fr) auto;
    gap:0 var(--sp3);align-items:baseline;margin-top:var(--sp4);padding-top:var(--sp3);
    border-top:1px solid var(--hair)}
  .cell.hero.ceiling .s .hero-best-k{font-size:var(--f1);font-weight:500;letter-spacing:.07em;
    text-transform:uppercase;color:var(--ink-subtle)}
  .cell.hero.ceiling .s .hero-best-v{font-size:var(--f5);font-weight:600;line-height:1.15;
    color:var(--ink);font-variant-numeric:tabular-nums;justify-self:start}
  .cell.hero.ceiling .s .hero-best-v.neg{color:var(--bad)}
  .cell.hero.ceiling .s .hero-best-s{grid-column:1/-1;margin-top:var(--sp1);
    color:var(--ink-subtle);font-weight:500}
  /* The premise sentence reads at the same weight as the scheme line it sits under: it is the
     frame every figure hangs on, not a footnote. The below-zero sentence carries the figure's
     own colour, because it is the reading of the red number beside it. */
  .cell.hero.ceiling .s .hero-premise{color:var(--ink-subtle);font-weight:500;
    margin-bottom:var(--sp1)}
  .cell.hero.ceiling .s .hero-below-zero{margin-top:var(--sp3);color:var(--bad);
    font-weight:500}
  .v.pos{color:var(--good)} .v.neg{color:var(--bad)} .v.nil{color:var(--ink-subtle)}
  .cell .v.phrase{font-size:var(--f5);letter-spacing:-.2px;line-height:1.25}

  /* ---------- recap: the pinned answer + one-click reach to any assumption ---------- */
  /* R2-the-edit-loop (AC1, AC2, AC5). His loop starts "from the summary" and stays there for
     three edits in a row -- so the figure that is being changed has to still be on screen once
     the reader has scrolled down into "Change any assumption" or any other drawer, and parking
     (the assumption he distrusts first) has to be one click away from wherever they are.
     `position:sticky` rather than a scroll-triggered `fixed` element: it sits inline at its own
     position on first paint (nothing new above the fold), then pins to the top of the viewport
     the moment scrolling would carry it past it -- no JS scroll listener, and it can never
     cover content before the reader has started scrolling past it. */
  .recap{position:sticky;top:0;z-index:35;display:flex;flex-wrap:wrap;align-items:center;
         justify-content:space-between;gap:var(--sp3);margin:var(--sp3) 0;
         padding:var(--sp3) var(--sp4);background:var(--s2);
         border:1px solid var(--hair-strong);border-radius:var(--r-md);
         box-shadow:0 4px 16px rgba(0,0,0,.45)}
  .recap[hidden]{display:none}
  .recap-main{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--sp1) var(--sp4);min-width:0}
  /* On desktop the main group shares the row with the buttons instead of being content-sized:
     left at flex-basis:auto, a long change line made this group's intrinsic width exceed the
     bar and pushed the buttons onto a second row -- measured live at 1280. flex:1 1 0 lets it
     shrink, and the change line inside it (same treatment below) absorbs all of the squeeze. */
  @media(min-width:641px){.recap-main{flex:1 1 0;flex-wrap:nowrap}}
  .recap-addr{font-size:var(--f2);color:var(--ink-subtle);white-space:nowrap;overflow:hidden;
              text-overflow:ellipsis;max-width:28ch}
  .recap-val{font-size:var(--f4);font-weight:600;color:var(--ink)}
  .recap-val b.neg{color:var(--bad)}
  /* R2 AC5: what moved, and because of what -- so a reader who only ever looks at the pinned
     bar can still tell an edit landed without scrolling back up to the strip or down into the
     waterfall to find the row that changed. Empty (no element rendered) until a second edit has
     something to compare against; see renderNumbers()'s own comment on why the first render
     after a fresh lookup never shows one. */
  /* Inline with the address and the ceiling, not a second row: flex-basis:100% grew the
     pinned bar to three rows the moment a change line appeared, which shoves the page down on
     every edit. It shrinks and ellipsizes instead -- the full sentence is transient status,
     and the figures it summarises are all live on the bar and the strip. The narrow-screen
     block below still lets it wrap, where a single row was never available anyway. */
  .recap-change{font-size:var(--f2);color:var(--accent-hi);flex:1 1 0;min-width:0;
                overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .recap-actions{display:flex;gap:var(--sp2);flex:none}
  .recap-btn{min-height:32px;padding:0 var(--sp3);font:inherit;font-size:var(--f2);
             color:var(--ink-muted);background:transparent;border:1px solid var(--hair-strong);
             border-radius:var(--r-sm);cursor:pointer;white-space:nowrap}
  .recap-btn:hover{color:var(--ink);border-color:var(--accent)}
  .recap-btn:focus-visible{outline:none;box-shadow:0 0 0 3px var(--accent-ring)}
  .recap-btn.primary{color:var(--accent-ink);background:var(--accent);border-color:transparent}
  .recap-btn.primary:hover{background:var(--accent-hi)}
  @media(max-width:640px){
    .recap{flex-direction:column;align-items:stretch}
    .recap-change{white-space:normal}
    .recap-actions{justify-content:stretch}
    .recap-btn{flex:1}
  }

  /* ---------- panels ---------- */
  /* The left column holds the waterfall and its captions; the right holds the map and the
     drawers, which are compact. Weighted toward the table rather than split evenly. */
  .cols{display:grid;grid-template-columns:1.2fr .8fr;gap:var(--sp3);margin-top:var(--sp3);
        align-items:start}
  .cols.single{grid-template-columns:minmax(0,1fr)}
  /* minmax(0,1fr), not 1fr. A bare 1fr keeps an automatic min-content floor, and the product
     tier <select> carries an option string long enough to force that floor to 420px inside a
     343px container -- so the whole page scrolled sideways by 160px on a phone and every panel
     clipped its right edge. .cols.single two lines up already had the correct form. */
  @media(max-width:920px){.cols{grid-template-columns:minmax(0,1fr)}}
  select,input{max-width:100%}
  /* 27 numeric fields in a 4-column grid, every one of them left-aligned despite carrying
     font-variant-numeric: tabular-nums. Tabular figures exist so digits line up in a column;
     left-aligning them means 3, 0.83, 6800 and 72250 all start at the same x and end nowhere
     near each other, so the metric was doing nothing at all. */
  .inputs input[type="number"]{text-align:right}
  .panel{padding:var(--sp6);background:var(--s1);border:1px solid var(--hair);border-radius:var(--r-lg)}
  .panel + .panel{margin-top:var(--sp3)}
  /* 19px/600 against 14px/400 body. Panel titles are the only landmarks in a column that
     runs nearly 4,000px, and at --f3 they were the same size as the numbers beneath them.
     The margin matters as much as the size: a title needs air above it to read as the start
     of something rather than as a caption on what came before. */
  .panel h3{margin:0 0 var(--sp3);font-size:var(--f5);font-weight:600;letter-spacing:-.01em;
            color:var(--ink)}
  .panel + .panel{margin-top:var(--sp5)}
  .panel .sub{margin:0 0 var(--sp4);font-size:var(--f2);line-height:1.5;color:var(--ink-subtle)}

  table{width:100%;border-collapse:collapse;font-size:var(--f3)}
  td{padding:var(--sp3) 0;border-bottom:1px solid var(--hair);vertical-align:top;color:var(--ink-muted)}
  tr:last-child td{border-bottom:none}
  td.n{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums;color:var(--ink)}
  /* Net sales revenue: a mid-waterfall subtotal, lighter than the final total. It exists so
     the two-stage shape is visible - commission comes off the top to reach this line, and
     everything below is measured against it. */
  tr.subtotal td{border-top:1px solid var(--hair);font-weight:600}
  tr.total td{padding-top:var(--sp3);border-top:1px solid var(--hair-strong);border-bottom:none;
              font-weight:600;color:var(--ink)}
  td .why{display:block;margin-top:var(--sp1);font-size:var(--f1);line-height:1.5;color:var(--ink-faint)}

  /* Magnitude bars, right-anchored so they grow leftward from the figures they belong to and
     the reader's eye tracks one edge instead of two. --mag is set by scaleWaterfallBars();
     the rule renders nothing at all until it is, so a row that never gets scaled shows no bar
     rather than a full-width one. */
  td.n[data-v]{position:relative}
  /* Anchored to the FIGURE, not to the bottom of the cell. At bottom:6px in a 68px cell the
     bar drew below the grey source line, ~40px from the number it belongs to and nearer the
     next row's divider than its own -- so it read as a stray rule rather than as a measure of
     the figure above it.
     min-width keeps the small lines visible: property transfer tax at 0.09% of hard cost
     rendered 0.11px, which is not a short bar, it is no bar, and "too small to see" and "not
     applicable" then look identical. */
  td.n[data-v]::after{content:'';position:absolute;right:0;top:calc(12px + 1.65em + 3px);
                      width:max(var(--mag,0), 2px);height:3px;
                      border-radius:2px;background:var(--ink-faint);opacity:.75}
  /* A faint full-width track, so a short bar reads as "small share of the biggest cost"
     instead of as a fleck. Without it there is nothing to be short RELATIVE TO. */
  td.n[data-v]::before{content:'';position:absolute;right:0;top:calc(12px + 1.65em + 3px);
                       width:100%;height:3px;
                       border-radius:2px;background:var(--ink-faint);opacity:.14}

  /* ONE SHARED HORIZONTAL SCALE for the joint range and for every single-input swing beneath
     it. The block used to make its central argument in prose -- that the low case is every
     input going wrong at once, and further down than any one of them can reach -- and asked
     the reader to hold five figures in their head to check it. On one axis it is just
     visible: no single bar reaches as far left as the joint span. The asymmetry the copy kept
     insisting on ("not a plus-or-minus band") stops needing to be insisted on, because a band
     drawn around a midpoint looks nothing like this.

     ONE GRID, and that is the whole point of this rule set. The first build drew the range and
     the swings as two separate blocks and captioned the second "on the same scale as the range
     above". They shared a value domain and not a pixel track -- 538px starting at x=201 above,
     244px starting at x=442 below -- so the two zeroes stood 79px apart, and sighting a bar up
     against the range landed nowhere near it. The caption described an alignment that did not
     exist. A shared grid column makes it true by construction instead of by intention.

     Percentage positioning rather than SVG: it reflows at any width and prints without a
     viewBox fight. */
  /* align-items:stretch, and that one word is load-bearing. Centred, a track kept its fixed
     17px inside whatever height the row grew to, so the one label long enough to wrap left a
     6px hole in the zero rule above and below it -- the rebuild whose entire point was to make
     that rule continuous shipped it broken in two places. Tracks now fill their row and the
     marks inside centre themselves, so a wrapped label costs a taller row and nothing else. */
  /* The axis column has a floor and the label column never breaks a word. Without the floor,
     in the 480px dock the label column took its 184px and the value column its max-content, and
     the axis -- the thing the chart is -- resolved to 34px, with LOW, CENTRAL and HIGH piled on
     one point. min-content on the label column is what keeps "construction contingency" whole
     when the floor squeezes it. */
  .rc{display:grid;grid-template-columns:minmax(min-content,184px) minmax(120px,1fr) minmax(42px,auto);
      gap:0 var(--sp3);align-items:stretch;margin-top:var(--sp3);padding:var(--sp4);
      background:var(--canvas);border:1px solid var(--hair);border-radius:var(--r-md)}
  .rc-k{font-size:var(--f1);line-height:1.35;color:var(--ink-subtle);text-align:right;
        align-self:center}
  /* Right-aligned. A currency column carrying tabular-nums and a ragged right edge is wearing
     the costume of alignment without the alignment. */
  .rc-v{font-size:var(--f1);color:var(--ink-faint);font-variant-numeric:tabular-nums;
        text-align:right;align-self:center}
  .rc-track{position:relative;min-height:17px}
  .rc-axis{min-height:32px}

  /* Zero is ONE continuous rule rather than a stack of per-row segments. Rows carry no vertical
     gap and each track paints a full-height slice, so the slices meet. It also paints ABOVE the
     bars: underneath them it was hidden on precisely the rows whose bars cross it, which are
     the rows where a break-even reference is worth having. And it is drawn at the axis
     marker's own weight instead of at 1.24:1 against the panel, which was a rule you could
     not see. */
  /* Full strength. At .45 the effective colour was rgb(70,73,78) -- 2.31:1, under the floor
     for a graphical object, on the most consequential reference in the chart. The red callout
     under the chart is entirely about whether the range crosses this line. */
  /* The halo is what keeps it visible WHERE A BAR CROSSES IT, which is the only place a
     break-even reference is interesting. Against the empty panel the rule is 8:1; laid over a
     bar it was 1.39:1 and effectively disappeared for the bar's width, in three rows of seven.
     Same trick the central tick already uses. */
  /* SIDEWAYS ONLY. A `0 0 0 1px` spread paints one pixel ABOVE each segment too, and the rule
     is nine stacked per-row segments, so every segment darkened the last pixel row of the one
     above it: eight notches at a 17px pitch, dropping the rule to rgb(40,41,44) at each. The
     DOM check passed the whole time -- the segment gaps really are 0.000px -- while the paint
     was a ladder. Geometry said solid and pixels said dashed, which is the very encoding that
     separates this rule from the estimate beside it. Two horizontal shadows do the one job the
     halo was for, which is holding the rule apart from a bar crossing it. */
  .rc-zero{position:absolute;top:0;bottom:0;width:1px;margin-left:-.5px;
           background:var(--ink-subtle);z-index:2;
           box-shadow:-1px 0 0 rgba(1,1,2,.75), 1px 0 0 rgba(1,1,2,.75)}
  /* THE CENTRAL ESTIMATE, carried down through the swing rows as a faded continuation of the
     white tick above. Every swing straddles it -- a bar runs from the residual with that one
     input at its pessimistic end to the residual with it at its optimistic end, and the
     central case sits between them by construction -- so the bars DID share an origin and it
     simply was not drawn. Without it, comparing two swings meant comparing the lengths of two
     segments starting at different x, which is close to the weakest judgement a reader can be
     asked to make, and the numbers in the right-hand column were doing the comparison while
     the bars only confirmed it. With it the lengths are read against a common line.
     Deliberately white where the zero rule is grey: one is the estimate, the other is
     break-even, and they can fall within 30px of each other. */
  /* DASHED, and that is the distinction -- not a difference in opacity.
     These two rules can run within 26px of each other for the whole height of the chart. The
     first attempt separated them by hue and alpha: white at .26 and grey-blue at .45 land on
     rgb(65,65,66) and rgb(70,73,78), which is 1.13:1 between them. Five RGB points at one
     pixel wide is not a distinction. A dash pattern is legible at any opacity, and the two
     rules now differ in kind: break-even is solid and structural, the estimate is dashed. */
  /* The dash is drawn here rather than delegated. `border-left:dashed` came out 4-on/1-off at
     one pixel -- an 82% duty cycle that reads as a solid hairline, so the two rules were
     converging on each other. An explicit 4-on/4-off gradient is unmistakable at any size.
     This is also the rule that needed the halo, and did not get it: over a bar it sat at
     1.78:1 while break-even, already 8:1 against the panel and backed by the red/green split,
     got the treatment. Alpha up, and the same sideways separation, because the caption tells
     the reader the bars are measured from THIS line. */
  .rc-mid{position:absolute;top:0;bottom:0;width:1px;margin-left:-.5px;z-index:2;
          background:repeating-linear-gradient(to bottom,
            rgba(247,248,248,.85) 0 4px, rgba(1,1,2,0) 4px 8px);
          box-shadow:-1px 0 0 rgba(1,1,2,.55), 1px 0 0 rgba(1,1,2,.55)}
  .rc-zero-cap{position:absolute;top:0;font-size:var(--f1);color:var(--ink-subtle);
               transform:translateX(-50%)}

  /* The break between red and green is placed AT zero rather than at the middle of the span.
     A three-stop gradient put its neutral tone at 50% of the bar while break-even was at 55%,
     so the colour called a loss neutral for 27px -- small, and this is a chart whose entire
     pitch is that its numbers are placed carefully. */
  .rc-span{position:absolute;top:50%;transform:translateY(-50%);height:10px;border-radius:5px;z-index:1}
  .rc-dot{position:absolute;top:50%;transform:translateY(-50%);width:2px;height:16px;
          margin-left:-1px;background:var(--ink);box-shadow:0 0 0 2px var(--canvas);z-index:3}
  /* 1.5px, not 4px. At a 4px radius the smallest bar was narrower than its own corner and
     rendered as a dot, so the mark type visibly changed a third of the way down the chart -
     which reads as an artifact rather than as an encoding. */
  .rc-bar{position:absolute;top:50%;transform:translateY(-50%);height:7px;border-radius:1.5px;
          background:var(--ink-faint);z-index:1}
  /* Emphasis has to be BRIGHTER, and it must not be paid for by everything else. The lead bar
     was --accent at 4.44:1 among bars at 5.75:1, so the one singled out was the hardest to
     see. The first fix dimmed the other eight to .75 to make the lead stand out, which bought
     the highlight by dropping eight data marks to 3.60:1 -- barely over the floor for a
     graphical object, on a 7px mark. The lead is brighter AND thicker; nothing else moves. */
  .rc-bar.lead{background:var(--accent-hi);height:9px}

  /* The three figures stand at their own positions on the axis. They used to sit in fixed
     left-to-right slots below it, so the central case printed at 30% of the box while its
     marker stood at 65% of the axis -- two pictures of the same three numbers, adjacent,
     disagreeing about where they were. Positions are swept apart when they would collide, so
     a narrow range stays readable. */
  /* The distribution block. Leads the uncertainty section, above the tornado. */
  .dist{margin-top:var(--sp3);padding:var(--sp4);background:var(--canvas);
        border:1px solid var(--hair);border-radius:var(--r-md)}
  .dist-head{font-size:var(--f4);color:var(--ink-muted);margin-bottom:var(--sp3)}
  .dist-head b{font-size:var(--f6);color:var(--ink);font-variant-numeric:tabular-nums}
  .dist-row{display:grid;grid-template-columns:minmax(90px,auto) 1fr auto;gap:var(--sp3);
            align-items:baseline;padding:var(--sp2) 0;border-top:1px solid var(--hair);
            font-size:var(--f2);color:var(--ink-faint)}
  .dist-row b{font-size:var(--f4);color:var(--ink);font-variant-numeric:tabular-nums;
              text-align:right}
  .dist-row span:last-child{text-align:right;font-size:var(--f1)}
  /* The layman headline of the uncertainty section: three figures in a developer's own
     dialect (downside / middle / upside), sized with the same tokens the .dist rows and
     .range labels already use. Pilot feedback 2026-09-03: the section led with statistician
     language ("Nth-percentile outcome") that the warm-lead developers it will be demoed to
     would need explained; the statistician rows now sit one layer down, and these three
     carry the headline. */
  .dist-tri{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--sp3);
            text-align:center;margin-bottom:var(--sp3)}
  .dist-tri .tri-k{display:block;font-size:var(--f1);font-weight:500;letter-spacing:.4px;
                   text-transform:uppercase;color:var(--ink-faint)}
  .dist-tri .tri-v{display:block;font-size:var(--f6);font-weight:600;line-height:1.3;
                   font-variant-numeric:tabular-nums;color:var(--ink)}
  .dist-tri .tri-v.neg{color:var(--bad)} .dist-tri .tri-v.pos{color:var(--good)}
  .dist-tri .tri-s{display:block;font-size:var(--f1);color:var(--ink-faint)}
  .dist-odds{margin:0 0 var(--sp2);padding-top:var(--sp3);border-top:1px solid var(--hair);
             font-size:var(--f3);color:var(--ink-muted)}
  .dist-odds b{color:var(--ink);font-variant-numeric:tabular-nums}

  .range{position:relative;height:36px}
  /* No transform here: it is set inline per label, because a label near an edge pivots on its
     point instead of centring over it. The suppressed path renders these with no inline style
     at all, which is why .range.plain can still lay them out flat. */
  .range-row{position:absolute;top:0;display:flex;flex-direction:column;align-items:center;
             gap:1px;white-space:nowrap}
  .range-k{font-size:var(--f1);font-weight:500;letter-spacing:.4px;text-transform:uppercase;
           color:var(--ink-faint)}
  .range-v{font-size:var(--f4);font-weight:600;line-height:1.2;font-variant-numeric:tabular-nums;
           color:var(--ink)}
  .range-v.neg{color:var(--bad)} .range-v.pos{color:var(--good)} .range-v.nil{color:var(--ink-subtle)}
  /* The suppressed path has no axis to stand on, so the labels lay out flat. */
  .range.plain{position:static;height:auto;display:flex;gap:var(--sp6);flex-wrap:wrap;
               margin-top:var(--sp3);padding:var(--sp4);background:var(--canvas);
               border:1px solid var(--hair);border-radius:var(--r-md)}
  .range.plain .range-row{position:static;transform:none;align-items:flex-start}
  /* Where the chart is narrow -- the 480px dock and the phone sheet, where the axis track is
     120px and the three labels need about 230px -- the labels stop standing on their points
     and lay out flat in order, as the suppressed strip does. Piled on one point they read as
     "LOWENTRAHIGH" over "−$11.49M$143K$4.75M" (measured at 400px). spreadRangeLabels() keeps
     writing its percentages; `position:static` makes them inert, and the inline transform is
     the one thing here that has to be overridden. */
  .rc{container-type:inline-size}
  @container (max-width:420px){
    .rc .range{position:static;height:auto;display:flex;justify-content:space-between;
               flex-wrap:wrap;gap:2px 12px}
    .rc .range .range-row{position:static;transform:none!important;align-items:flex-start}
  }
  /* Beside the rule, not centred on it. Centred, the solid rule ran through the hyphen of the
     word naming it and the dashed rule through its last letter. A caret ties it to the line it
     belongs to without either rule touching the text. */
  .rc-foot{position:absolute;top:2px;margin-left:5px;font-size:var(--f1);
           color:var(--ink-subtle);white-space:nowrap}
  .rc-foot::before{content:'';position:absolute;left:-5px;top:6px;width:4px;height:1px;
                   background:var(--ink-subtle)}

  /* ---------- R4-presentation: the cash flow drawer -- four-number strip, filled-area
     carry-tail chart, quarterly table. Reuses .strip's own .cell subgrid shape (label/value/
     caption rows) rather than inventing a second tile component -- a four-column variant is
     just .strip with its 6-column template overridden. */
  .cf-strip{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));
            grid-template-rows:auto auto auto;margin-top:var(--sp3);background:var(--s1);
            border:1px solid var(--hair);border-radius:var(--r-lg);overflow:visible}
  .cf-strip .cell:first-child{border-radius:var(--r-lg) 0 0 var(--r-lg)}
  .cf-strip .cell:last-child{border-radius:0 var(--r-lg) var(--r-lg) 0}
  .cf-off{padding:var(--sp4);background:var(--s1);border:1px solid var(--hair);
          border-radius:var(--r-md);font-size:var(--f3);line-height:1.55;color:var(--ink-subtle)}
  .cf-off b{color:var(--ink)}
  .cf-chart{margin-top:var(--sp5)}
  .cf-chart svg{width:100%;height:170px;display:block;overflow:visible}
  .cf-chart-caption{margin-top:var(--sp2);font-size:var(--f1);color:var(--ink-faint)}
  .cf-table{width:100%;border-collapse:collapse;margin-top:var(--sp4);font-size:var(--f3)}
  /* AC3 density fix: a collapsed middle-carry row (schedules over 12 quarters) reads as a
     summary, not a real quarter's data -- muted + italic, matching .cf-chart-caption's own
     de-emphasis vocabulary. The completion quarter (wherever it lands, head/tail/visible row)
     gets the same warn-line the chart's own dashed completion marker uses, so the two surfaces
     share one visual vocabulary for "this is where the build finishes." */
  .cf-table tr.cf-q-collapsed td{color:var(--ink-faint);font-style:italic}
  .cf-table tr.cf-q-completion td{border-top:2px solid var(--warn-line)}
  .cf-table th{text-align:right;font-size:var(--f1);font-weight:500;color:var(--ink-subtle);
               padding:var(--sp2);border-bottom:1px solid var(--hair-strong);white-space:nowrap}
  .cf-table th:first-child,.cf-table td:first-child{text-align:left}
  .cf-table td{text-align:right;padding:var(--sp2);border-bottom:1px solid var(--hair);
               font-variant-numeric:tabular-nums;color:var(--ink-muted);white-space:nowrap}
  .cf-note{margin-top:var(--sp5);padding:var(--sp3) var(--sp4);font-size:var(--f2);
           line-height:1.6;color:var(--ink-subtle);background:var(--s2);
           border:1px solid var(--hair);border-radius:var(--r-md)}
  .cf-note + .cf-note{margin-top:var(--sp2)}
  .cf-note b{color:var(--ink)}
  .cf-note.gap{color:var(--bad-ink);background:var(--bad-tint);border-color:var(--bad-line)}
  .cf-note.gap b{color:var(--bad-ink)}
  /* AC8's lender precondition must read as an assumption/precondition, not a computed result --
     visually, not just in prose. Reuses .flag's own amber vocabulary (warn-tint/warn-line/
     warn-ink) so it reads the same "this is a stated assumption, not this tool's own answer" as
     every ASSUMPTION chip elsewhere on the page. AC4's interestOutputSentence (a genuinely
     computed schedule output) stays on plain .cf-note -- the neutral box is now the "this tool
     computed it" signal by contrast. */
  .cf-note.precondition{color:var(--warn-ink);background:var(--warn-tint);border-color:var(--warn-line)}
  .cf-note.precondition b{color:var(--warn-ink)}

  /* ONE CHIP SHAPE, and the word carries the hue.
     Every state used to bring its own tinted block -- green for measured, amber for
     assumption, indigo for derived -- which spent a coloured area on a CATEGORICAL dimension
     and left green meaning three unrelated things on one page: this figure was measured, this
     entitlement is likely approvable, and this money is above break-even. The tints are also
     the wrong editorial: amber says "warning" about an operator input the whole screen is
     built on deliberately.
     A neutral chip with coloured TEXT keeps the ladder scannable at a glance while cutting the
     coloured area to the glyphs, so the two loud greens left on the page - the verdict band
     and money above zero - are large, few, and unmistakable for each other. */
  .tag{display:inline-block;margin-left:var(--sp2);padding:0 var(--sp1);line-height:15px;
       border-radius:var(--r-xs);font-size:10px;font-weight:600;letter-spacing:.4px;
       text-transform:uppercase;vertical-align:1px;
       background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.09)}
  .t-measured{color:var(--good-ink)}
  /* Deliberately not the accent: the accent means brand, action and focus. A data category
     wearing it made "cited" the dominant use of the brand colour on screen. */
  .t-cited{color:#c3cad4}
  .t-assumption{color:var(--warn-ink)}
  /* A figure the City answered earlier, marked on the figure rather than only on the card
     below it. Amber, like every other "this is about our read" mark on this page. */
  .t-recorded{color:var(--warn-ink);border-color:var(--warn-line)}
  /* The layer cache's own chip (SPEC review 2026-09-2x, finding 2): a saved read of one City
     layer, not a curated demo recording -- same family mark, so it still reads as "about our
     read", never as a finding about the land. */
  .t-layer-cache{color:var(--warn-ink);border-color:var(--warn-line)}
  /* Dashed, because the record is not closed. The one state where the chip's own edge can say
     something the word is also saying. */
  .t-none{color:var(--ink-faint);border-style:dashed;background:transparent}
  /* The fourth provenance state. The ceiling is not measured, not cited and not an assumption
     -- it is a computation over all three, and leaving it unchipped was the one gap that made
     the row unscannable as a layer. */
  .t-derived{color:var(--accent-hi)}

  .inputs{display:grid;grid-template-columns:repeat(auto-fit,minmax(136px,1fr));gap:var(--sp3)}
  .inputs label{display:block;margin-bottom:var(--sp1);min-height:28px;font-size:var(--f1);
                letter-spacing:.3px;color:var(--ink-subtle);line-height:1.3}
  /* R1-every-assumption-editable: `.toggles input[type="number"]` reaches stalls-override (pre-
     existing) and the new nullable/market-line-override inputs this ring adds -- a numeric
     control living outside `#inputs` (a select, a checkbox, a nullable override) is still one of
     this page's assumption controls and gets the same visible-message treatment (AC6). */
  .inputs input,.toggles input[type="number"]{width:100%;min-height:36px;padding:var(--sp2);font:inherit;font-size:var(--f3);
                color:var(--ink);background:var(--canvas);border:1px solid var(--hair);
                border-radius:var(--r-sm);font-variant-numeric:tabular-nums;transition:border-color .12s ease}
  .inputs input::-webkit-outer-spin-button,
  .inputs input::-webkit-inner-spin-button,
  .toggles input[type="number"]::-webkit-outer-spin-button,
  .toggles input[type="number"]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
  .inputs input,.toggles input[type="number"]{-moz-appearance:textfield}
  .inputs input:hover,.toggles input[type="number"]:hover{border-color:var(--hair-strong)}
  .inputs input:focus,.toggles input[type="number"]:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-ring)}
  /* The slot is neutral; only a real validation message is red. It used to carry --bad on the
     container, so the provenance note living in the same slot inherited the error colour, and
     a single deleted child element would bring the red back. A rule that is only correct
     because something else happens to override it is not a rule. */
  .inputs .limit,.toggles .limit{display:block;margin-top:2px;font-size:var(--f1);color:var(--ink-faint);min-height:1em}
  .inputs .limit.invalid,.toggles .limit.invalid{color:var(--bad)}
  /* R2-the-profile-on-the-screen. The per-key profile marker sits in its OWN slot beside the
     validation slot (.limit), never inside it: a validation message and a provenance marker are
     two different claims about the same control, and sharing one slot means every rejected
     keystroke wipes the profile sentence (and restoring it redraws over the error). Same shape
     the drawer already gives .derived notes, plus the accent so a profile-supplied figure reads
     as attributed rather than as a system default. */
  .profile-mark{display:block;margin-top:2px;font-size:var(--f1);color:var(--accent-hi);min-height:0}
  .profile-mark:empty{display:none}
  .profile-mark[hidden]{display:none}
  .profile-banner{margin:var(--sp2) 0;padding:var(--sp2) var(--sp3);font-size:var(--f2);
                  color:var(--ink);background:var(--s2);border:1px solid var(--hair);
                  border-radius:var(--r-md)}
  .profile-strip{display:inline-flex;align-items:center;padding:var(--sp1) var(--sp3);
                 font-size:var(--f2);color:var(--accent-hi);background:var(--s2);
                 border:1px solid var(--hair);border-radius:999px}
  /* R2 iteration 3 (AC6): the dedicated clear-the-active-profile control, same pill shape as
     the other profile-row buttons. Only ever visible while the banner is (renderProfileBanner
     toggles both together), so "a control that moves nothing" cannot appear with nothing to
     clear. */
  .profile-clear{display:inline-block;margin:var(--sp2) 0;min-height:32px;padding:0 var(--sp3);
                 font:inherit;font-size:var(--f2);color:var(--ink-subtle);background:transparent;
                 border:1px solid var(--hair);border-radius:999px;cursor:pointer}
  .profile-clear:hover{color:var(--ink);border-color:var(--accent)}
  .profile-clear[hidden]{display:none}
  .profile-panel{margin:0 0 var(--sp4);padding:var(--sp3);background:var(--s2);
                 border:1px solid var(--hair);border-radius:var(--r-md)}
  .profile-panel .why{margin:0 0 var(--sp2)}
  .profile-actions{display:flex;flex-wrap:wrap;gap:var(--sp2);margin-top:var(--sp2)}
  .profile-actions button,.profile-actions .file-btn,.profile-row button
    {min-height:32px;padding:0 var(--sp3);font:inherit;font-size:var(--f2);color:var(--ink-subtle);
     background:transparent;border:1px solid var(--hair);border-radius:999px;cursor:pointer}
  .profile-actions button:hover,.profile-actions .file-btn:hover,.profile-row button:hover
    {color:var(--ink);border-color:var(--accent)}
  .file-btn{display:inline-flex;align-items:center;cursor:pointer}
  .profile-save-form{margin-top:var(--sp2);display:flex;flex-wrap:wrap;gap:var(--sp2);align-items:center}
  .profile-save-form input{min-height:32px;padding:0 var(--sp2);font:inherit;font-size:var(--f2);
    color:var(--ink);background:var(--s1);border:1px solid var(--hair-strong);
    border-radius:var(--r-sm);max-width:22em}
  .profile-note{margin-top:var(--sp2);font-size:var(--f2);color:var(--warn-ink,#7a5b00);
                min-height:0}
  .profile-note:empty{display:none}
  .profile-list{margin-top:var(--sp2)}
  .profile-row{display:flex;flex-wrap:wrap;gap:var(--sp2);align-items:baseline;
               padding:var(--sp2) 0;border-top:1px solid var(--hair)}
  .profile-row .pname{font-size:var(--f2);color:var(--ink)}
  .profile-row .pdate{font-size:var(--f1);color:var(--ink-faint)}
  .share-choice{position:fixed;inset:0;z-index:60;display:flex;align-items:center;justify-content:center;
                background:rgba(0,0,0,.45)}
  .share-choice[hidden]{display:none}
  .share-choice-card{max-width:26em;margin:var(--sp4);padding:var(--sp5);background:var(--s2);
                     border:1px solid var(--hair-strong);border-radius:var(--r-lg)}
  .share-choice-card p{margin:0 0 var(--sp3);font-size:var(--f3);color:var(--ink);font-weight:600}
  .share-choice-card .why{font-weight:400;color:var(--ink-subtle);font-size:var(--f2)}
  .share-choice-card button{display:block;width:100%;margin:var(--sp2) 0;min-height:38px;
    padding:0 var(--sp3);font:inherit;font-size:var(--f2);color:var(--ink-subtle);
    background:transparent;border:1px solid var(--hair-strong);border-radius:var(--r-sm);cursor:pointer}
  .share-choice-card button:hover{color:var(--ink);border-color:var(--accent)}
  /* --ink3 does not exist in this token set, so this rule did nothing and the note inherited
     --bad from .limit above it: a benign "from this project" provenance note was painted in
     the error colour, in the validation-message slot, next to inputs that use the same token
     for aria-invalid. Four more rules referenced the same dead token. */
  .inputs .derived{color:var(--ink-faint)}
  /* The band change is the only control that waits on the server. Dim the figures it is about
     to replace, so a stale number is visibly stale rather than quietly wrong. */
  #out.recalculating{opacity:.45;transition:opacity .12s}
  #out.recalculating::before{content:'Recalculating for the new storey band…';display:block;
    margin-bottom:var(--sp3);font-size:var(--f2);color:var(--ink-subtle)}
  @media(prefers-reduced-motion:reduce){#out.recalculating{transition:none}}
  button.lever{display:inline-block;margin-top:var(--sp3);background:transparent;
    border:1px solid var(--hair);color:inherit;padding:5px 10px;border-radius:6px;
    cursor:pointer;font:inherit;font-size:var(--f1)}
  button.lever:hover{border-color:var(--ink-faint)}
  .inputs input[aria-invalid="true"],.toggles input[aria-invalid="true"]{border-color:var(--bad);background:var(--bad-tint)}
  .inputs input[aria-invalid="true"]:focus,.toggles input[aria-invalid="true"]:focus{border-color:var(--bad);box-shadow:0 0 0 3px rgba(242,104,108,.25)}
  .reset{margin-top:var(--sp4);min-height:32px;padding:var(--sp2) var(--sp3);font:inherit;
         font-size:var(--f2);color:var(--ink-subtle);background:transparent;
         border:1px solid var(--hair);border-radius:var(--r-sm);cursor:pointer;transition:all .12s ease}
  .reset:hover{color:var(--ink);border-color:var(--hair-strong);background:var(--s2)}

  /* EVERY select, not just the two that happened to get styled.
     #product-tier, #parking-basis and #size-gradient were raw browser defaults sitting inside
     the same panel as these: 19px tall against 28px, a mid-grey #3B3B3B fill on a near-black
     panel, a #858585 border, no radius, and the browser's own 13.33px type. #hard-cost-guide
     sits about 100px above #product-tier, so both treatments were visible in one screenful.
     That mismatch, rather than any margin, is what made this block look broken. */
  select{min-height:32px;padding:6px 26px 6px var(--sp3);font:inherit;font-size:var(--f2);
         color:var(--ink);background:var(--canvas);border:1px solid var(--hair);
         border-radius:var(--r-sm);cursor:pointer;transition:border-color .12s ease;
         color-scheme:dark;
         /* Native chevrons on Windows render as a light-on-light wedge in a dark control, so
            appearance:none and draw our own in the text colour. */
         appearance:none;-webkit-appearance:none;
         background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),
                          linear-gradient(135deg,currentColor 50%,transparent 50%);
         background-position:calc(100% - 15px) 55%, calc(100% - 10px) 55%;
         background-size:5px 5px, 5px 5px;
         background-repeat:no-repeat}
  select:hover{border-color:var(--hair-strong)}
  select:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-ring)}
  select:disabled{opacity:.55;cursor:wait}
  /* Sliders and checkboxes were drawing Chrome's own control colour - a black track and
     thumb under the page's dark color-scheme. The action accent is the colour this screen
     already uses for anything the reader operates. */
  input[type=range],input[type=checkbox],input[type=radio]{accent-color:var(--accent)}
  /* A disabled control keeps the shape it had; only its weight drops. Left to the UA it
     turned into a grey box that matched nothing else on the screen. */
  button:disabled{opacity:.45;cursor:default}
  /* The option list is drawn by the OS, which does not inherit the panel's colours. */
  select option{background:var(--s1);color:var(--ink)}
  #band-select{min-height:28px}

  /* Label sits 2px off its control while the next group is 32px away, so label, control and
     helper text read as one mush rather than three related things. Even it out. */
  .inputs > div > label,.inputs > div > .k{display:block;margin-bottom:6px}
  .inputs > div{margin-bottom:var(--sp4)}
  .guide-row label{display:block;margin-bottom:var(--sp1);font-size:var(--f1);letter-spacing:.3px;
                    color:var(--ink-subtle);line-height:1.3}
  .guide-row .why{display:block;margin-top:2px;font-size:var(--f1);line-height:1.5;color:var(--ink-faint)}

  .sug{margin-bottom:var(--sp2);padding:var(--sp3);background:var(--canvas);
       border:1px solid var(--hair);border-radius:var(--r-md)}
  .sug .h::before{content:"";display:inline-block;width:5px;height:5px;margin-right:var(--sp2);
                  border-radius:1px;background:var(--accent-hi);vertical-align:2px}
  .sug:last-child{margin-bottom:0}
  .sug.alert{border-color:var(--warn-line);background:var(--warn-tint)}
  .sug .h{margin-bottom:var(--sp1);font-size:var(--f3);font-weight:500;color:var(--ink)}
  .sug .b{font-size:var(--f2);line-height:1.55;color:var(--ink-subtle)}
  .sug .cite{margin-top:var(--sp2);font-size:var(--f2);color:var(--ink-subtle);
             font-family:ui-monospace,SFMono-Regular,"Cascadia Mono",Consolas,monospace}
  .sug .delta{font-weight:600;color:var(--good)}

  #map{width:100%;height:320px;border:1px solid var(--hair);border-radius:var(--r-md);
       background:var(--canvas);z-index:0}
  .map-fallback{display:flex;align-items:center;justify-content:center;height:100%;
                padding:var(--sp4);text-align:center;color:var(--ink-subtle);font-size:var(--f2)}
  #map .leaflet-control-attribution{background:rgba(1,1,2,.7);color:var(--ink-faint);font-size:9px}
  #map .leaflet-control-attribution a{color:var(--ink-subtle)}
  #map .leaflet-control-layers{background:var(--s3);border:1px solid var(--hair-strong);
       border-radius:var(--r-sm);color:var(--ink-muted)}
  #map .leaflet-bar a{background:var(--s3);color:var(--ink);border-color:var(--hair-strong)}
  #map .leaflet-bar a:hover{background:var(--s2)}
  /* R2-the-map-labels-every-edge (AC1): the same dark-casing-over-bright-fill legibility
     discipline as the lot and envelope lines above, expressed as a solid pill instead of a
     text shadow -- a pill reads over ANY imagery (roof, concrete, gravel, water) the way a
     casing line does, where a text shadow alone can still wash out on a busy image. White
     text on a near-black, slightly translucent fill; the conservative variant carries the
     envelope's own accent colour on its left edge so a reader can tell at a glance which
     method drew the label without reading the legend first. */
  .setback-label-wrap{background:transparent;border:none}
  .setback-label{display:inline-block;padding:1px 6px;border-radius:3px;
    background:rgba(11,14,18,.86);color:#fff;font-size:11px;font-weight:600;
    white-space:nowrap;box-shadow:0 0 0 1px rgba(255,255,255,.25);
    font-variant-numeric:tabular-nums;transform:translate(-50%,-50%)}
  .setback-label.conservative{box-shadow:0 0 0 1px rgba(255,255,255,.25),
    inset 3px 0 0 var(--accent-hi)}
  .step{padding:3px 0 3px 20px;position:relative;color:var(--ink-faint);font-size:var(--f2);
        line-height:1.5}
  .step::before{content:"";position:absolute;left:4px;top:9px;width:6px;height:6px;
                border-radius:50%;background:var(--hair-strong)}
  .step.now{color:var(--ink-muted)}
  .step.now::before{background:var(--accent-hi);animation:sh 1s ease-in-out infinite}
  .step.done{color:var(--ink-subtle)}
  .step.done::before{background:var(--good)}
  #ac .ac-none{color:var(--ink-subtle);cursor:default;font-size:var(--f2)}
  #ac .ac-none:hover{background:transparent;color:var(--ink-subtle)}
  .plot-cap{margin:var(--sp3) 0 0;text-align:center;font-size:var(--f2);color:var(--ink-subtle);
            font-variant-numeric:tabular-nums}
  .plot polygon{fill:rgba(94,106,210,.18);stroke:var(--accent-hi);stroke-width:1.4;
                vector-effect:non-scaling-stroke}
  .plot-empty{display:flex;align-items:center;justify-content:center;width:100%;max-width:260px;
              height:150px;margin:0 auto;background:var(--canvas);border:1px solid var(--hair);
              border-radius:var(--r-md);color:var(--ink-subtle);font-size:var(--f2)}

  /* DRAWERS. Long explanation collapses to one line with a status on it.
     The page had four blocks of 40-to-90 words of continuous prose that a reader either read in
     full or skipped in full -- and the parts of them that matter are different for every
     address, because a downtown UC parcel and a suburban four-plex settle a different set of
     questions. A summary line carries the state for THIS address and the detail waits behind
     it. Native <details>, so it works with no JavaScript, is keyboard-operable, and prints
     expanded when the print stylesheet opens them. */
  /* THE ANSWER, ABOVE THE ACCORDIONS.
     Reads as the sentence the whole page is making: what it sells for, what it costs, what is
     left for the land. The third item is the product and is weighted accordingly; the first
     two exist to show it is a subtraction rather than an opinion. */
  .sumline{display:flex;align-items:stretch;gap:var(--sp3);margin-bottom:var(--sp5);
           padding:var(--sp4);background:var(--s2);border:1px solid var(--hair);
           border-radius:var(--r-md);flex-wrap:wrap}
  /* min-content, not 0. At `min-width:0` the item shrank under its own figure and the figure
     broke to fit it: `$13,688,000` rendered as "$13,688,00" over "0" in the 480px dock at 1440px
     and 1920px. A figure is one token; the item that holds it is at least as wide as it, and the
     row wraps instead. */
  .sl-item{flex:1 1 150px;min-width:min-content;display:flex;flex-direction:column;gap:2px}
  .sl-k{font-size:var(--f1);color:var(--ink-subtle)}
  .sl-n{font-size:var(--f6);font-weight:600;letter-spacing:-.5px;color:var(--ink);
        font-variant-numeric:tabular-nums;line-height:1.15}
  .sl-item.hero .sl-n{font-size:var(--f7)}
  .sl-n.neg{color:var(--bad)}
  .sl-s{font-size:var(--f1);color:var(--ink-faint);line-height:1.4}
  .sl-op{flex:none;align-self:center;font-size:var(--f5);color:var(--ink-faint);
         padding:0 var(--sp1)}
  .sumline .sl-item.hero{flex-basis:190px}
  @media(max-width:640px){ .sl-op{display:none} .sl-item{flex-basis:100%} }
  /* The ledger card's download door, full-width directly under the equation it exports --
     pulled up against the sumline (whose own margin-bottom is generous) so the two read as
     one unit: the answer, then the file that carries it. */
  .sl-download{margin:calc(-1*var(--sp3)) 0 var(--sp4)}
  .sl-download .recap-btn{width:100%;min-height:38px;color:var(--ink);
                          border-color:var(--accent)}
  .sl-download .recap-btn:hover{color:#fff;background:var(--accent)}

  /* The one accordion that is an INVITATION rather than a reference. Everything else on this
     page is something to read; this is the thing to touch, and a developer arguing with our
     numbers is the entire point of the tool. It gets an accent edge so it does not read as the
     third of three identical grey rows. */
  .drawer.section.invite{border-color:var(--accent-ring)}
  .drawer.section.invite > summary{position:relative}
  .drawer.section.invite > summary::before{content:'';position:absolute;left:0;top:8px;bottom:8px;
                                           width:2px;border-radius:2px;background:var(--accent-hi)}
  .drawer.section.invite > summary:hover{background:var(--accent-tint,rgba(120,130,255,.06))}
  .drawer.section.invite .d-t{color:var(--ink)}

  /* SECTION ACCORDIONS.
     The page had one very tall panel per column and asked the reader to scroll past everything
     to find anything. The fix is not less information, it is a summary that answers the
     question and a body that shows the working. `.d-v` is the part that makes a closed
     accordion useful: the headline figure sits on the summary row, so the reader gets the
     answer without opening anything and opens only what they want to argue with. */
  .drawer > summary > .d-v{flex:none;margin-left:auto;padding-left:var(--sp3);
                           font-size:var(--f3);font-weight:600;color:var(--ink);
                           font-variant-numeric:tabular-nums;white-space:nowrap}
  .drawer > summary > .d-v + .d-v{margin-left:0}
  /* With a value present the chevron must not also claim margin-left:auto, or the two fight
     and the value drifts off the right edge. */
  .drawer > summary:has(.d-v)::after{margin-left:var(--sp3)}

  /* The heavier variant, for the three or four real sections of the page rather than for a
     footnote. Bigger hit area, a title at reading size, and a rule under the summary when open
     so the body reads as belonging to it. */
  .drawer.section{background:transparent;border-color:var(--hair)}
  .drawer.section > summary{padding:var(--sp4);gap:var(--sp3)}
  .drawer.section .d-t{font-size:var(--f4);font-weight:600}
  .drawer.section[open] > summary{border-bottom:1px solid var(--hair);
                                  border-radius:var(--r-md) var(--r-md) 0 0}
  .drawer.section > .d-b{padding:var(--sp4)}
  .drawer.section + .drawer.section{margin-top:var(--sp3)}
  /* A closed accordion should look like it opens. Without this the chevron is the only
     affordance and it is 7px of hairline. */
  .drawer.section > summary:hover .d-t{color:var(--ink)}
  .drawer.section > summary::after{border-color:var(--ink-muted)}

  .drawer{border:1px solid var(--hair);border-radius:var(--r-md);background:var(--s1)}
  .drawer + .drawer{margin-top:var(--sp2)}
  .drawer > summary{display:flex;align-items:baseline;gap:var(--sp3);padding:var(--sp3) var(--sp4);
                    cursor:pointer;list-style:none;border-radius:var(--r-md)}
  .drawer > summary::-webkit-details-marker{display:none}
  /* --s2 over --s1 is five RGB points, a 1.06:1 change: the hover state existed and did
     nothing. The border is what a reader is already looking at on a bordered control. */
  .drawer > summary:hover{background:var(--s2);box-shadow:inset 0 0 0 1px var(--hair-strong)}
  .drawer > summary:hover::after{border-color:var(--ink)}
  .drawer > summary:focus-visible{outline:2px solid var(--accent-ring);outline-offset:-2px}
  .drawer > summary::after{content:'';flex:none;align-self:center;margin-left:auto;
    width:7px;height:7px;border-right:1.5px solid var(--ink-subtle);
    border-bottom:1.5px solid var(--ink-subtle);transform:translateY(-2px) rotate(45deg);
    transition:transform .15s ease}
  .drawer[open] > summary::after{transform:translateY(1px) rotate(-135deg)}
  .d-t{font-size:var(--f4);font-weight:600;color:var(--ink)}
  .d-s{font-size:var(--f1);color:var(--ink-faint)}
  .d-b{padding:0 var(--sp4) var(--sp4);font-size:var(--f2);line-height:1.65;color:var(--ink-subtle)}
  .d-b > * + *{margin-top:var(--sp3)}
  /* The profile controls sit in one host node so the phone shell can move them whole. display:contents keeps the
     drawer laying out as if the wrapper were not there; the same sibling spacing is restated for its children. */
  .profile-controls{display:contents}
  .profile-controls > * + *{margin-top:var(--sp3)}
  .d-b ul{margin:0;padding-left:var(--sp5)}
  .d-b li{margin:var(--sp2) 0}
  .d-b li + li{margin-top:var(--sp3)}
  /* What would close a gap, under the gap it closes. This is the part a reader who wants to
     know whether the tool is going anywhere actually reads.
     It was styled identically to the line above it -- same size, same weight, same colour, same
     measure -- so the gap and its remedy merged into one grey block, and the only thing setting
     them apart was a "Next:" prefix rendered BRIGHTER than the sentence it introduced. The
     label popped and the content receded, which is backwards. The rule is the separator now,
     the prefix is quiet, and the sentence is the brightest thing in the pair. */
  .d-next{display:block;margin-top:var(--sp2);padding-top:var(--sp2);
          border-top:1px dashed var(--hair);font-size:var(--f1);color:var(--ink-faint)}
  .d-next::before{content:'Next';display:inline-block;margin-right:var(--sp2);
                  padding:0 var(--sp1);border-radius:var(--r-xs);
                  background:rgba(255,255,255,.05);font-size:var(--f1);letter-spacing:.3px;
                  text-transform:uppercase;color:var(--ink-faint)}

  .limits{margin-top:var(--sp3);padding:var(--sp5);background:var(--s1);border:1px solid var(--hair);
          border-radius:var(--r-lg);font-size:var(--f2);line-height:1.6;color:var(--ink-subtle)}
  .limits strong{display:block;margin-bottom:var(--sp2);font-size:var(--f3);font-weight:500;color:var(--ink)}
  .limits ul{margin:0;padding-left:var(--sp5)} .limits li{margin:var(--sp1) 0}
  footer{margin-top:var(--sp7);max-width:80ch;font-size:var(--f2);line-height:1.7;color:var(--ink-subtle)}
  [data-help]{position:relative;cursor:help}
  [data-help]::after{content:attr(data-help);position:absolute;left:0;top:calc(100% + 6px);
    z-index:50;width:max-content;max-width:320px;padding:var(--sp3);background:var(--s3);
    border:1px solid var(--hair-strong);border-radius:var(--r-md);color:var(--ink-muted);
    font-size:var(--f2);font-weight:400;line-height:1.55;letter-spacing:0;text-transform:none;
    opacity:0;visibility:hidden;transition:opacity .12s ease;pointer-events:none}
  /* Hover waits ~450ms before showing; hiding stays immediate (the base transition carries no
     delay). Without it, moving the pointer across the strip raised a paragraph over every
     figure it crossed. Keyboard focus stays immediate -- someone who tabbed to the element
     asked for it. */
  [data-help]:hover::after{transition-delay:.45s}
  [data-help]:hover::after,[data-help]:focus-visible::after{opacity:1;visibility:visible}
  tr[data-help]::after{left:var(--sp4)}
  .cell:nth-child(n+5)[data-help]::after{left:auto;right:0}
  .legend{margin:var(--sp3) 0 0;font-size:var(--f2);color:var(--ink-subtle);line-height:1.7}
  /* Each note on its own line. Run in as inline text they read as one long sentence about
     nothing; the reader needs to see that they are two separate caveats. */
  .legend-note{display:block;margin-top:var(--sp2);font-size:var(--f1);color:var(--ink-faint);
               line-height:1.55}
  .legend .sw{display:inline-block;width:11px;height:11px;margin:0 var(--sp2) 0 0;
              border-radius:2px;vertical-align:-1px}
  /* The key for the lot was a white box with a white border on a white panel, so the row
     began with nothing. It now reads as the map draws it: the parcel stroke around the
     casing colour. */
  .legend .sw.lot{border:2px solid var(--map-parcel-stroke,#1F2328);background:var(--map-parcel-casing,#FFFFFF)}
  .legend .sw.env{border:2px dashed var(--constraint-setback-ink,#155E9B);background:var(--constraint-setback-fill,#E5EFF8);
                  margin-left:var(--sp4)}
  .purpose{margin:var(--sp4) 0 0;font-size:var(--f2);line-height:1.6;color:var(--ink-subtle)}
  .purpose .q{display:block;margin-bottom:var(--sp1);color:var(--ink-faint)}
  .dd{list-style:none;margin:0;padding:0}
  .dd li{display:flex;gap:var(--sp3);align-items:baseline;padding:var(--sp2) 0;
         border-bottom:1px solid var(--hair);font-size:var(--f2)}
  .dd li:last-child{border-bottom:none}
  .dd li::before{flex:none;width:14px;font-size:11px}
  .dd li.yes::before{content:"✓";color:var(--good)}
  .dd li.no::before{content:"·";color:var(--ink-faint)}
  .dd .dd-k{flex:none;width:170px;color:var(--ink-muted)}
  /* No override on the name. Dimming it for open items put it on the same token as its own
     value, so all twelve rows in "Still open" collapsed into one flat grey block -- and the
     bug hid because "Settled here" uses li.yes, where the ladder still descended. Open versus
     settled is what the marker is for; the name of a thing is equally the name of it either
     way. Descent is name, then what is missing, then what would close it. */
  .dd .dd-v{color:var(--ink-subtle)}
  @media(max-width:600px){.dd li{flex-wrap:wrap}.dd .dd-k{width:auto}}
  .hidden{display:none}

  @media(max-width:920px){
    /* Hero spans, then the remaining cells (five, or six with the FAR tile) in two columns.
       The divider is drawn with a left border on the even-positioned cells so it lands
       between them, never on the container edge. .strip.with-far outranks the bare .strip
       selector even in here, so it is named explicitly or it would keep seven columns. */
    .strip,.strip.with-far{grid-template-columns:repeat(2,minmax(0,1fr))}
    .cell{border-left:none;border-top:1px solid var(--hair)}
    .cell.hero{grid-column:1/-1;border-top:none}
    .cell:nth-child(odd):not(.hero){border-left:1px solid var(--hair)}
    .cell:last-child:nth-child(even){grid-column:1/-1}
    /* The cash-flow strip carries FIVE cells, so its last one lands in an odd slot and would
       otherwise sit in a half-width column beside a gap. */
    .cf-strip .cell:last-child:nth-child(odd){grid-column:1/-1}
  }
  @media(max-width:600px){
    .wrap{padding:var(--sp6) var(--sp4) 56px}
    .search,.empty,.note.err{max-width:none}
    .search-row{flex-direction:column}
    #go,.chip,.inputs input,.reset{min-height:44px}
    .call .word{font-size:var(--f6)}
    .cell.hero .v{font-size:var(--f7)}
    .verdict,.panel{padding:var(--sp5)}
    /* Leaflet lays its tiles out beyond the container, so without this the whole page
       scrolled sideways about 160px on a phone -- the classic broken-mobile feel, on the
       device a broker actually has in his hand standing on the site. */
    #map,.mapwrap{max-width:100%;overflow:hidden}
  }
  /* R2-the-edit-loop: `html` ONLY, never `html,body` together. Setting overflow-x on BOTH
     disqualifies the standard root/body overflow-propagation rule (which only propagates
     body's overflow to the viewport when the root's own computed overflow is 'visible' in
     both axes) -- so `body` ends up as its OWN, separate non-scrolling overflow box nested
     inside `html`'s, and any `position:sticky` descendant sticks against BODy's inert
     scrollport rather than the one that is actually scrolling. Measured live: the recap bar
     below (AC1/AC2) computed `position:sticky;top:0` correctly and never moved off `top:0` on
     scroll while `body` also carried `overflow-x:hidden` -- removing `body` here, and nothing
     else, fixed it. `html` alone still clips the whole document horizontally, which is the
     entire point of this rule (the Leaflet sideways-scroll fix above). */
  html{overflow-x:hidden}

  .stamp{display:block;margin-top:var(--sp3);opacity:.65}
  /* `hidden` is a UA rule of display:none, and ANY author display rule outranks it. So this
     row -- which the markup marks hidden and the script only reveals once a result exists --
     was visible on the empty page the whole time, with three buttons that did nothing when
     clicked. Found by driving the real page rather than by reading it. The guard belongs here
     rather than on this one selector: every author display rule in this file has the same
     hole, and the next one added would reopen it. */
  [hidden]{display:none !important}
  .actions{display:flex;gap:var(--sp3);margin-top:var(--sp4);flex-wrap:wrap}
  .actions button{background:transparent;border:1px solid var(--hair);color:inherit;
    padding:6px 12px;border-radius:6px;cursor:pointer;font:inherit;font-size:var(--f2)}
  .actions button:hover{border-color:var(--ink-faint)}
  .actions button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

  /* Print. Ctrl+P used to give a client seven pages of near-black, or with backgrounds off,
     light grey text on white -- invisible. A broker's only route to a client was a screenshot
     of a dark dashboard, which does not look like work product. Print is the one context
     where this page must not be dark. */
  @media print{
    /* REMAP the ramp, do not invert it. The previous block redefined --bg, --ink2 and --ink3,
       none of which this page uses, so the real tokens kept their dark-theme values: every
       element with an explicit --ink-subtle or --ink-faint printed pale on white while
       containers were force-darkened. The effect was an inverted hierarchy -- proforma row
       labels lighter than their own 11px rationale captions, and the money column the lightest
       marks in a printed table row. These are the names the page actually reads. */
    :root{
      --canvas:#fff; --s1:#fff; --s2:#fff; --s3:#fff;
      --hair:#c7c7cc; --hair-strong:#8e8e93;
      --ink:#111; --ink-muted:#1c1c1e; --ink-subtle:#48484a; --ink-faint:#6b6b70;
      --accent:#2f3a8f; --accent-hi:#2f3a8f; --accent-ring:transparent;
      --good:#1d6b47; --warn:#7a5312; --bad:#8c2f33;
      --good-tint:#fff; --warn-tint:#fff; --bad-tint:#fff;
      --good-line:#1d6b47; --warn-line:#7a5312; --bad-line:#8c2f33;
      /* The dark-theme -ink variants (used for text-on-tint, e.g. .cf-note.gap, .flag,
         .t-measured/.t-assumption, and the chart's completion label) were never remapped here,
         so they kept their pale dark-theme values (#8fdcb8/#f0cb90/#f7a5a7) on forced-white
         print backgrounds -- ~1.5-1.9:1 contrast, under the 3.0:1 AA floor. Reuse the same
         darker good/warn/bad values above; they were already print-safe. */
      --good-ink:#1d6b47; --warn-ink:#7a5312; --bad-ink:#8c2f33;
    }
    *{-webkit-print-color-adjust:exact;print-color-adjust:exact}
    html,body{background:#fff !important;color:#111 !important}
    .wrap{padding:0;max-width:none}
    /* Anything you cannot act on from paper. A sticky bar means nothing on a page with no
       scroll position, and pinning it would print the same strip on every physical page. */
    .search,.chips,#ac,.actions,.reset,#go,.inputs input,.inputs select,.inputs .limit,
    .toggles select,.toggles input,.recap{display:none !important}
    /* The stamp says to quote it alongside any number taken from the page, so it has to
       survive onto the page someone actually carries into a meeting. */
    footer .stamp{display:block !important;color:#444 !important}
    .panel,.verdict,.strip,.cell,.sug,.flag,.toggles{background:#fff !important;
      color:#111 !important;border-color:#bbb !important;box-shadow:none !important;
      break-inside:avoid}
    .sug *,.flag *{color:#111 !important}
    /* Paper has no disclosure triangle. Every drawer prints open, or the printed page silently
       loses the part that says what the screen could not check. */
    .drawer > .d-b{display:block !important}
    .drawer > summary::after{display:none}
    .drawer{border-color:#bbb !important;break-inside:avoid}
    .d-t{color:#111 !important} .d-s,.d-next{color:#444 !important}
    /* A chart split across a page break is a chart with half an axis. And the bars ARE
       backgrounds, so they depend on the color-adjust rule above surviving; without it the
       printed chart is a grid of empty tracks with a caption explaining what is in them. */
    .rc{break-inside:avoid}
    .tag{border:1px solid #999 !important;color:#333 !important;background:#fff !important}
    a{color:#111 !important;text-decoration:underline}
    /* The assumptions are the argument, so they print as read-only values rather than
       vanishing with their input boxes. */
    .inputs .printed-value{display:inline !important}
    #map{max-height:9cm}
    .why,.s{color:#444 !important}
  }
  .inputs .printed-value{display:none;font-variant-numeric:tabular-nums}
  .toggles{margin-top:var(--sp4)}
  /* The help text sits INSIDE each label, and with nothing telling it otherwise it flowed
     inline right after the label text. In a narrow column beside a second control the eye then
     reads across the pair -- "This scheme needs a rezoning or | Adds the amenity contribution.
     Off" -- which looks like a rendering fault even though it is not. Its own line, one step
     quieter, so a label reads as a label and its explanation reads as an explanation. */
  .toggles label{display:block;margin-top:var(--sp4);font-size:var(--f2);color:var(--ink-muted)}
  .toggles .why{display:block;margin-top:var(--sp1);font-size:var(--f1);line-height:1.55;
                color:var(--ink-faint)}
  .toggles .check{display:flex;flex-wrap:wrap;gap:0 var(--sp3);align-items:flex-start;
                  margin-top:var(--sp4)}
  .toggles .check input{margin-top:3px;flex:none}
  .toggles .check .why{flex-basis:100%;margin-left:calc(13px + var(--sp3))}
  .toggles .gradient-note{display:block;margin-top:4px}

/* Comparison numbers need two columns in the docked Site panel. */
.strip.assembly-comparison{grid-template-columns:repeat(2,minmax(0,1fr))}
.assembly-comparison .cell{min-width:0}
.assembly-comparison .v{overflow-wrap:anywhere}

/* Address fallback: the confirmation stays legible inside the narrow Site drawer. */
.assembly-address { margin-top: var(--sp4); }
.assembly-address label { display:block; margin:var(--sp3) 0 var(--sp2); }
.assembly-address input { display:block; width:100%; box-sizing:border-box; padding:var(--sp2); margin-bottom:var(--sp3); color:var(--ink); background:var(--s1); border:1px solid var(--hair); border-radius:6px; font:inherit; }
.assembly-address input:focus-visible { outline:2px solid currentColor; outline-offset:2px; }
.assembly-address li { overflow-wrap:anywhere; }
.assembly-card.selected{border-color:var(--accent);background:var(--s3)}
.assembly-combined-site{margin:var(--sp3) 0;padding:var(--sp3);border:1px solid var(--hair-strong);border-radius:4px}

/* Assembly has four independent metrics; it must not inherit the six-column verdict strip.
   Auto-fit responds to its actual dock width, not only the window width. */
.assembly-comparison{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,190px),1fr));gap:var(--sp3)}
.assembly-comparison .cell{grid-row:auto;display:flex;flex-direction:column;min-width:0;padding:var(--sp3);border:1px solid var(--hair);border-radius:var(--r-lg);overflow-wrap:anywhere}
.assembly-comparison .v{font-size:var(--f3);font-weight:600;line-height:1.2;margin-block:var(--sp2)}
.hero-zone-gloss{display:inline}
/* A glossary token inside a heading is part of the sentence. Measured at 768 and 400 on
   2026-09-20: the phone shell's blanket `:is(button,...){min-height:44px!important;font-size:16px}`
   drew MF3 six pixels smaller than the 22px words around it, inside a 44px box, as a visible
   stair-step mid-sentence. Inside the verdict headline the token inherits its size and its line
   and carries nothing but the dotted rule; the 44px tappable copy of the same definition is the
   zone chip in the same card. (The phone shell does not mount this trigger at all -- see
   workspace.mjs -- so no tap target is shrunk here; the shape is stated for wherever it renders.) */
.hero-zone-trigger{font:inherit;color:inherit;background:none;border:0;border-bottom:1px dotted currentColor;padding:0;cursor:pointer}
.call .word .hero-zone-trigger{font-size:inherit;line-height:inherit;font-weight:inherit;min-height:0;min-width:0;padding:0;vertical-align:baseline}
.hero-zone-trigger:focus-visible{outline:2px solid currentColor;outline-offset:3px}
.hero-zone-description{position:fixed;z-index:2000;box-sizing:border-box;max-width:min(24rem,calc(100vw - 24px));overflow:auto;padding:12px 16px;border:1px solid var(--hair-strong);border-radius:6px;background:var(--s1);color:var(--ink);font:400 14px/1.5 var(--font-sans, sans-serif);box-shadow:0 4px 18px #0003;text-align:left}
.hero-zone-description[hidden]{display:none}
@media print{.hero-zone-trigger{border:0}.hero-zone-description{display:none!important}}

/* An informational empty result must not cover a Try chip's hit target. The whole
   overlay, not just its disabled row, yields pointer/touch events to the control below. */
#ac.ac-empty,#ac.ac-empty *{pointer-events:none}

/* R2 shared Site view owner. The deterministic still is generated on demand. */
#massing-region{min-width:0;padding:0 8px 8px} .massing-region-canvas{height:clamp(360px,65vh,720px)} .massing-region-still svg{width:100%;height:auto} .massing-context-caption{padding:0 12px} #massing-region[hidden]{display:none}

.massing-viewport button{font:inherit;border:1px solid var(--control-line);border-radius:5px;padding:6px 10px} .massing-region-still{padding:0 12px}

.massing-facts{padding:0 12px;display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:4px 12px}.massing-facts p{margin:2px 0;font-size:14px}.massing-facts .massing-warning{color:var(--warn-ink);background:var(--warn-tint);padding:8px;grid-column:1/-1}
.tag[data-explain],.term[data-explain]{cursor:help}.term[data-explain]{text-decoration:underline dotted;text-underline-offset:2px}.tag-explain-note{position:fixed;z-index:2000;box-sizing:border-box;max-width:min(24rem,calc(100vw - 24px));padding:12px 16px;border:1px solid var(--hair-strong);border-radius:6px;background:var(--s1);color:var(--ink);font:400 14px/1.5 var(--font-sans, sans-serif);box-shadow:0 8px 24px rgba(0,0,0,.25)}.tag-explain-note[hidden]{display:none}@media print{.tag-explain-note{display:none!important}}
