/* Typography and contrast for the actual audience: working appraisers,
   average age 60-65, on a desktop, often against a deadline. 17px body,
   generous line height, no thin greys, no animation.

   U19 sets a HARD FLOOR of 16px on every piece of text a person reads, which
   is the unit's Definition of Done. Secondary text (column headers, sample
   values, the step breadcrumb, warning codes, the chart legend) used to run
   13-15px and now does not. If you are tempted to drop something below 16px
   to win back horizontal room, shorten the words instead.

   The ONE deliberate exception is text inside the chart SVG (`.chart .axis`,
   `.chart .complab`, the effective-date label, and the matching `font-size`
   attributes in app/chart.py, all 13px). That is figure annotation, not body type: the geometry in
   chart.py computes its margins and label spacing against that size.

   What makes the exception safe is that nothing the appraiser must READ to
   check the work lives only there. Every comparable's label, contract date,
   adjustment, dollars and interval is repeated in the adjustments table at
   16px+, and the window and trends are in the summary panel. What is genuinely
   chart-only is scale annotation -- the axis tick labels -- which is read by
   position against the plotted points, not by the digits. If you ever move a
   value INTO the chart and nowhere else, this exception stops being safe.

   U19c adds the spacing, border and state scales below. Three rules that are
   load-bearing rather than cosmetic:

   1. EVERY colour pair in here clears WCAG AA (4.5:1), and every pair carrying
      body or label text clears 7:1. `tools/check_contrast.py` computes them and
      fails on a regression -- run it after touching any colour. "No thin greys"
      is this project's oldest UI rule and the reason `--muted` is #4a5464 and
      not something a slate palette would suggest.
   2. Control padding is set so an input and a button are the SAME HEIGHT
      (48px). They sit side by side on S6 and in the S2 footer, and a 6px
      mismatch there was the single most visible unfinished edge in the app.
   3. Focus is a box-shadow ring, never a border or width change, so focusing a
      control cannot reflow the row it lives in. */
:root {
  /* Ink and surface. Contrast ratios in the comment are against --bg unless
     stated; see tools/check_contrast.py for the full computed table. */
  --ink: #14181f;           /* 17.8:1 */
  --muted: #4a5464;         /*  7.7:1 -- the floor for label text, do not lighten */
  --line: #e4e9ef;          /* hairlines only, never text */
  --line-strong: #cdd5df;   /* hairlines that must carry a control edge */
  /* Surfaces are inverted from the original: the PAGE carries the faint tint
     and CARDS are white, rather than white page + grey-filled panels. Grey
     fills read as "disabled" and are what made the app look dated; a white
     card on a tinted page reads as depth. The tint stays cool, not the warm
     stone a neutral palette would suggest, because the chart's blues are
     baked into app/chart.py and cannot be rebalanced with it. */
  --bg: #f7f9fb;
  --card: #ffffff;
  --panel: #f4f7fa;         /* only where a fill still carries meaning */
  --accent: #14507d;        /*  8.5:1 */
  --accent-ink: #0f3d5f;    /* 11.3:1 -- hover/active only */
  --accent-wash: #eef3f8;   /* selected/hover fills, never behind small text */
  --focus: rgba(20, 80, 125, .28);
  --warn-bg: #fff6e5; --warn-line: #d99100; --warn-ink: #8a5d00; /* 5.4:1 on --warn-bg */
  --bad: #a8261e;           /*  7.1:1 */
  --good: #17603a;          /*  7.6:1 */

  /* 4px base, 8px rhythm. Every margin, padding and gap in this file resolves
     to one of these -- if a value is not on the scale it is a bug, not taste. */
  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px;
  --s5: 24px; --s6: 32px; --s7: 48px;

  /* Softer than the 6/8/10 this replaces. 10px on a control and 16px on a card
     is the current idiom (the reference product ships --radius: .625rem = 10px
     on controls with larger cards); 6px reads as 2015 chrome. */
  --r1: 10px; --r2: 12px; --r3: 16px;

  /* One control height for inputs, selects and buttons, stated rather than
     derived. 26px line box + 10px padding top and bottom + 1px border = 48px.
     It has to be stated because Chrome's UA stylesheet forces `line-height:
     normal` on a `select` with appearance:auto and ignores any value set here,
     so a select laid out purely from padding lands 4px short of an input on
     the same row. min-height closes that without appearance:none, which would
     mean hand-drawing the caret. */
  --control-h: 48px; --control-lh: 26px;

  /* One elevation step. Panels are separated by their border; the shadow only
     lifts things that genuinely float above the page. */
  --shadow-sm: 0 1px 2px rgba(20, 24, 31, .04), 0 1px 3px rgba(20, 24, 31, .06);
  /* The hover card genuinely floats over the chart, so it gets the second and
     only other elevation step. Deeper than --shadow-sm because it sits above
     a busy scatter rather than above flat page. */
  --shadow-lg: 0 8px 16px rgba(20, 24, 31, .10), 0 2px 6px rgba(20, 24, 31, .08);

  /* An inverted surface, not another white card. Over a scatter of ~240 pale
     dots a white panel reads as one more element on the page; a dark one reads
     as floating above it, which is what it is doing. It also buys the highest
     contrast available to an audience in its sixties (ADR 0017). */
  --tip-bg: #1b212b;
  --tip-ink: #ffffff;       /* 15.9:1 on --tip-bg */
  --tip-muted: #b8c2d0;     /*  8.5:1 on --tip-bg -- label text floor is 7:1 */
}
/* Inter, self-hosted. Two reasons it is not a Google Fonts <link>: an appraiser
   working from a title office on flaky wifi should not wait on fonts.gstatic.com
   to read a number, and a request to a third party on every page view is a
   privacy claim this product does not want to have to make. The files are the
   Google-built variable subsets (weight axis 400-700, one file each for latin
   and latin-ext, 133KB total) under the SIL Open Font Licence 1.1 -- the licence
   text ships beside them in app/static/fonts/Inter-LICENSE.txt, which the OFL
   requires.

   `font-display: swap` is deliberate: the system stack below renders
   immediately and Inter swaps in. The alternative, `block`, gives invisible
   text for up to 3s, which for this audience is a broken page.

   NOTE: Inter's metrics are wider than -apple-system's at 17px. The S4
   adjustments table's fit inside its 692px track was re-measured after this
   change, not assumed -- see ADR 0017. */
@font-face {
  font-family: "Inter"; font-style: normal; font-weight: 400 700;
  font-display: swap; src: url("/static/fonts/inter-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Inter"; font-style: normal; font-weight: 400 700;
  font-display: swap; src: url("/static/fonts/inter-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
* { box-sizing: border-box; }
body {
  margin: 0; background: var(--bg); color: var(--ink);
  font: 17px/1.55 "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
        sans-serif;
  -webkit-font-smoothing: antialiased;
}
/* Form controls do not inherit type by default; without this the lookback
   selector on S4 rendered at the browser's 13px. */
button, input, select, textarea { font-family: inherit; font-size: inherit; }

/* Body links. Without this rule they render at the browser default #0000EE
   while the nav renders --accent, which is two different link colours on the
   same page. The underline stays: colour alone is not an affordance. */
a {
  color: var(--accent); text-decoration: underline;
  text-underline-offset: 2px; text-decoration-thickness: 1px;
}
a:hover { color: var(--accent-ink); }
:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--r1);
}

header.top {
  border-bottom: 1px solid var(--line); padding: var(--s3) var(--s6);
  display: flex; align-items: baseline; gap: var(--s5);
}
header.top h1 { font-size: 19px; margin: 0; letter-spacing: -0.2px; }
/* The product name is the way home: `/` is a new analysis when signed in and
   redirects to signup when not. Ink, not link blue, so the heading still reads
   as the name; the underline on hover says it is a link. */
header.top h1 a { color: inherit; text-decoration: none; }
header.top h1 a:hover { text-decoration: underline; }
header.top nav a { font-size: 16px; text-decoration: none; }
header.top nav a:hover { text-decoration: underline; }
header.top nav a + a { margin-left: var(--s5); }
main { max-width: 1180px; margin: 0 auto; padding: var(--s5) var(--s6) var(--s7); }
h2 { font-size: 25px; margin: 0 0 var(--s1); letter-spacing: -0.4px; }
h3 { font-size: 18px; margin: var(--s6) 0 var(--s3); }
p.lede { color: var(--muted); margin: 0 0 var(--s5); font-size: 16px; }

/* Spacing utilities. These exist so templates stop carrying one-off
   `style="margin-top:14px"`, which is how the rhythm drifted in the first
   place. Anything a template needs should be on the scale above. */
.stack   { margin-top: var(--s4); }
.stack-s { margin-top: var(--s3); }
.stack-l { margin-top: var(--s5); }
.stack-b { margin-bottom: var(--s5); }
.tight   { margin: var(--s2) 0 0; }
.narrow  { max-width: 520px; }
.inline  { display: inline; }
/* A row of controls that belong together: the search box and its button, or a
   primary action and its "go back" link. The gap replaces the margin-left the
   templates used to carry, which is what let the spacing drift apart. */
.actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3); }

/* Step breadcrumb. One accent at two weights reads as a progression; the
   earlier green "done" state put a second hue in the header for no gain. */
.steps { display: flex; gap: var(--s2); list-style: none; padding: 0;
         margin: 0 0 var(--s5); font-size: 16px; color: var(--muted);
         flex-wrap: wrap; }
.steps li { padding: var(--s1) var(--s3); border: 1px solid var(--line);
            border-radius: 999px; }
.steps li.on { background: var(--accent); color: #fff; border-color: var(--accent);
               font-weight: 600; }
.steps li.done { border-color: var(--accent); color: var(--accent);
                 background: var(--accent-wash); }

/* The upload zone. Everything here sits on ONE centred vertical axis with one
   gap value, because the previous version stacked a raw `input[type=file]` and
   a styled button in two <p>s: the native control renders as a small grey
   UA button plus "no file chosen" at ~24px tall, next to a 48px branded
   button, on different left edges and with no shared height, alignment or
   type. That mismatch was the most visibly unfinished thing in the app.
   `::file-selector-button` is what lets the native control be dressed to match
   without JavaScript or a hidden-input trick that breaks keyboard focus. */
.drop { border: 1px dashed var(--line-strong); border-radius: var(--r3);
        padding: var(--s7) var(--s5); background: var(--card);
        display: flex; flex-direction: column; align-items: center;
        gap: var(--s4); }
.drop-file { display: flex; flex-direction: column; align-items: center;
             gap: var(--s3); cursor: pointer; }
.drop-label { font-size: 19px; font-weight: 600; }
.drop-hint { margin: 0; color: var(--muted); font-size: 16px; }
.drop input[type=file] { font-size: 17px; max-width: 100%; }
.drop input[type=file]::file-selector-button {
  font: inherit; font-weight: 600; margin-right: var(--s3);
  padding: 10px 20px; line-height: var(--control-lh);
  min-height: var(--control-h); border: 1px solid var(--accent);
  border-radius: var(--r1); background: var(--card); color: var(--accent);
  cursor: pointer;
}
.drop input[type=file]::file-selector-button:hover {
  background: var(--accent-wash); border-color: var(--accent-ink);
  color: var(--accent-ink);
}
.drop input[type=file]:focus-visible::file-selector-button {
  outline: 2px solid var(--accent); outline-offset: 2px;
}

/* --- controls -------------------------------------------------------------
   Buttons, inputs and selects all resolve to --control-h. They sit side by
   side on S6 and in the S2 footer, and the 6px mismatch there was the single
   most visible unfinished edge in the app. */
button, .btn {
  font: inherit; font-weight: 600; padding: 10px 20px; border-radius: var(--r1);
  line-height: var(--control-lh); min-height: var(--control-h);
  border: 1px solid var(--accent); background: var(--accent); color: #fff;
  cursor: pointer; text-decoration: none; display: inline-block;
  text-align: center;
}
button:hover, .btn:hover {
  background: var(--accent-ink); border-color: var(--accent-ink); color: #fff;
}
button.secondary, .btn.secondary { background: var(--card); color: var(--accent); }
button.secondary:hover, .btn.secondary:hover {
  background: var(--accent-wash); color: var(--accent-ink);
  border-color: var(--accent-ink);
}
button:focus-visible, .btn:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
  box-shadow: 0 0 0 3px var(--focus);
}
button:disabled, button[disabled] {
  opacity: .5; cursor: not-allowed; background: var(--accent);
  border-color: var(--accent);
}
button:disabled:hover { background: var(--accent); border-color: var(--accent); }

/* Every text-entry control, not just the two that used to be listed here. The
   old selector was `.field select, input[type=text], input[type=date]`, which
   scoped `select` to .field -- so the S4 lookback select, sitting in a .panel,
   rendered as an unstyled native control -- and omitted password and search
   entirely. */
input[type=text], input[type=date], input[type=password], input[type=email],
input[type=search], input[type=number], select, textarea {
  font: inherit; padding: 10px 12px; border: 1px solid var(--line-strong);
  border-radius: var(--r1); background: var(--card); color: var(--ink);
  line-height: var(--control-lh); min-height: var(--control-h);
  /* Digits in a form column must line up with digits in the table below it. */
  font-variant-numeric: tabular-nums;
}
textarea { min-height: calc(var(--control-h) * 2); }
/* `height`, not `min-height`. Chrome gives input[type=date] an intrinsic
   content box of 28px (the calendar-picker indicator is taller than a 26px
   text line box), so min-height is simply exceeded and the control lands at
   50px beside a 48px text input in the same S3 row. */
input[type=date] { height: var(--control-h); }
/* A comfortable default width, but ONLY where the markup has not asked for a
   specific one. The blanket `min-width: 260px` this replaces overrode every
   size attribute in the app; on S3 it pushed the size="8" CBSA input onto its
   own line, 33px below the label it belongs to. */
input[type=text]:not([size]), input[type=date]:not([size]),
input[type=password]:not([size]), input[type=email]:not([size]) {
  min-width: 260px;
}
input:hover, select:hover, textarea:hover { border-color: var(--muted); }
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--focus); /* ring, not width: never reflows the row */
}
input:disabled, select:disabled, textarea:disabled {
  background: var(--panel); color: var(--muted); cursor: not-allowed;
}
input::placeholder { color: var(--muted); opacity: .75; }
label { display: inline-block; }

/* A label wrapping its own control, laid out as a row of such pairs. S3's
   subject row is three of these; before this they were inline-block labels
   separated by inline margin-left, and the third pair broke across lines. */
.row { display: flex; flex-wrap: wrap; gap: var(--s4) var(--s5);
       align-items: flex-end; }
.row label { display: flex; flex-direction: column; gap: var(--s1);
             font-size: 16px; color: var(--muted); }
.row label input, .row label select { align-self: flex-start; }

table { border-collapse: collapse; width: 100%; font-size: 16px;
        background: var(--card); border: 1px solid var(--line);
        border-radius: var(--r3); overflow: hidden; }
/* Row air is bought VERTICALLY only. The S4 adjustments table is seven columns
   inside a 692px track; widening the horizontal cell padding by 4px adds ~56px
   to the table's min-content, and a `display: table` overflows its grid track
   rather than shrinking inside it -- which put the table underneath the summary
   rail's export button. Vertical padding costs no width. */
th, td { text-align: left; padding: var(--s4) var(--s2);
         border-bottom: 1px solid var(--line); }
/* 16px floor. Uppercase plus letter-spacing is expensive in width at this
   size, and the adjustments table on S4 is seven columns inside a 698px
   left rail at 1280 -- so the tracking comes off rather than the size.
   vertical-align matters at seven columns: "95% interval" and "From your
   comps" wrap to two and three lines while "Days" stays on one, and without
   a shared baseline the header row reads as three separate rows. */
th { font-size: 16px; letter-spacing: 0; color: var(--muted); font-weight: 600;
     vertical-align: bottom; }
td { vertical-align: top; }
tr:hover td { background: var(--panel); }
tr:last-child td { border-bottom: 0; }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
/* An interval is one value. Without nowrap "-2.6% to +5.4%" breaks after "to"
   and the two bounds read as separate numbers in separate rows.
   HEADERS are deliberately left wrappable: the S4 adjustments table is seven
   columns in a 692px rail at 1280, and pinning the headers to one line each
   pushes the table's min-content to 804px, which shoves the summary rail off
   the right edge of the page. Headers wrap; `th { vertical-align: bottom }`
   above is what makes a wrapped header row read as one row. */
td.num { white-space: nowrap; }
/* Header cells claim the width of their text rather than being re-wrapped by
   auto table layout, which otherwise hands the last column only whatever the
   others leave (75px measured) and breaks a short heading into four ragged
   lines. This is why .grid2's rail is 328px and not the original 400: the S4
   table's last column needs ~112px for "From comps" on one line, and the 692px
   track that a 400px rail leaves does not have it. Measured, not estimated --
   a ONE-line 10-character heading is 13px WIDER than the longest line of the
   two-line "Weight on / local sales" it replaced, so shortening a heading can
   cost width rather than save it. Re-measure after any change here. */
th[class~="num"] { white-space: nowrap; }
/* A date is one value; at the 16px floor "2025-02-24" otherwise breaks after
   the hyphen and reads as two numbers. */
td.date, th.date { white-space: nowrap; }
td input { min-width: 0; width: 100%; }
/* A currency input under a right-aligned "Sale price" header has to be right
   aligned too, or the header and the digits it labels sit at opposite edges of
   the same column. */
td.num input { text-align: right; }

/* Adjustments table (S4). Its first column became a STREET ADDRESS in ADR
   0039 -- "Comp 1 — 14320 Southwest Hillsborough St" rather than "Comp 1" --
   and every other column is `nowrap`, so the table's min-content width grew
   past its 692px grid track and overflowed by 21px. CLAUDE.md records that
   this table has zero slack; the label column is the one that can give.
   `tools/audit_layout.py` only caught it once its fixture stopped using
   "Comp 1", the one label that can never be too long. */
.adjust td:first-child { overflow-wrap: anywhere; }

/* Comparables table (S3). A long control in the first column takes the row
   under automatic layout and squeezes the exhibit label down to about four
   visible characters, so the one control that is too wide is constrained.

   Fixed layout was tried first and is WRONG here: Chrome gives
   `input[type=date]` an intrinsic minimum its shadow content enforces, which
   `width: 100%` and `min-width: 0` cannot shrink, so any column narrower than
   that let the control hang 34px out over its neighbour. Constraining the one
   control that is actually too wide leaves the date columns to claim the
   minimum they genuinely need. `tools/audit_layout.py` checks `td input` and
   `td select` against their cells since ADR 0039, which is what settled this. */
.comps select { width: 15rem; max-width: 100%; }
/* `visible`, not the `hidden` every table inherits. The street list
   (`.combo-pop`) opens below its box inside this table, and the last row has
   no table under it: on comparable 4, 383px of a 396px list were clipped at
   the table's bottom edge, measured in Chrome. Without the clip the corner
   cells must round themselves, or the row hover fill (`tr:hover td`) paints
   square corners over the table's rounded ones. */
table.comps { overflow: visible; }
table.comps tr:first-child > :first-child { border-top-left-radius: var(--r3); }
table.comps tr:first-child > :last-child { border-top-right-radius: var(--r3); }
table.comps tr:last-child > :first-child { border-bottom-left-radius: var(--r3); }
table.comps tr:last-child > :last-child { border-bottom-right-radius: var(--r3); }

/* The comparable picker (ADR 0076): ONE box per row, the dropdown button
   inside it, and a list of suggestions that opens beneath. The box inherits
   `--control-h` from the shared input rule -- the layout audit asserts a
   single control height across the page -- and the two buttons drawn inside
   it are that same height, so they pass the same rule rather than being
   exempted from it. Room is made for them with padding, never by shrinking
   the box. */
.combo { position: relative; width: 15rem; max-width: 100%; }
.combo-box { position: relative; }
/* `input.compfind`, not `.compfind`: the shared `input[type=text]:not([size])`
   rule's 260px min-width outranks a bare class, and the box then spilled 20px
   out of its own 240px wrapper into the label column. */
.comps input.compfind { width: 100%; min-width: 0; padding-right: 48px; }
.comps .picked input.compfind { padding-right: 92px; font-weight: 600; }
.combo-btn { position: absolute; top: 0; width: 44px; padding: 0;
  min-height: var(--control-h); height: var(--control-h);
  border: 0; background: transparent; color: var(--muted);
  display: flex; align-items: center; justify-content: center; }
.combo-btn:hover, .combo-btn:focus-visible { background: transparent;
  color: var(--accent-ink); }
.combo-toggle { right: 2px; }
.combo-clear { right: 46px; }
/* The list floats over the rows beneath rather than pushing them down: a row
   that grew by eight suggestions while someone typed would move the Analyse
   button out from under the pointer. Wider than its box, because each
   suggestion carries a street AND the figures that tell two sales apart. */
.combo-pop { position: absolute; z-index: 20; left: 0; top: calc(100% + 4px);
  width: 30rem; max-width: calc(100vw - 2 * var(--s4));
  background: var(--card); border: 1px solid var(--line-strong);
  border-radius: var(--r1); box-shadow: 0 8px 24px rgba(20, 24, 31, .14); }
.combo-pop[hidden] { display: none; }
.combo-pop [role=listbox] { list-style: none; margin: 0; padding: var(--s1) 0;
  max-height: 22rem; overflow-y: auto; }
.combo-opt { display: flex; flex-direction: column; gap: 2px; cursor: pointer;
  padding: var(--s2) var(--s3); color: var(--ink); }
.combo-opt.active, .combo-opt:hover { background: var(--accent-wash); }
.combo-opt.active { box-shadow: inset 3px 0 0 var(--accent); }
.opt-addr { font-weight: 600; }
.opt-meta { color: var(--muted); font-variant-numeric: tabular-nums; }
/* --muted on --accent-wash is 6.86:1, under the 7:1 floor this text is held
   to, so a highlighted suggestion's figures go to --ink. */
.combo-opt.active .opt-meta, .combo-opt:hover .opt-meta { color: var(--ink); }
.combo-pop .hint { max-width: none; margin: 0; padding: var(--s2) var(--s3);
  border-top: 1px solid var(--line); }
.combo-pop .hint:empty { display: none; }
/* A figure the export supplies, shown for checking and not for editing. It
   is TEXT, not a disabled field, so it is never submitted -- and it reads as
   a value rather than as a control the appraiser should try to click. */
.fixed { display: inline-block; min-height: var(--control-h);
  padding: 10px 0; line-height: var(--control-lh); font-weight: 600;
  font-variant-numeric: tabular-nums; white-space: nowrap; }
.fixed.none { color: var(--muted); font-weight: 400; }

/* `.hint` states what the picker is showing. It is 16px because the audit
   refuses anything smaller, and `--muted` because that pair is already on
   `check_contrast.py`'s 7:1 list -- the audience is 60-65 (ADR 0017). */
.hint { margin: var(--s2) 0 0; color: var(--muted); font-size: 16px;
        max-width: 15rem; }
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
       font-size: 16px; }

/* --- "what does this column mean" disclosure -----------------------------
   Native popover, no JavaScript. Three reasons it is the right mechanism here
   rather than an absolutely-positioned div or a `title` attribute:

   1. The adjustments table sets `overflow: hidden` to clip its rounded
      corners, so anything positioned inside it is CLIPPED. A popover renders
      in the top layer and is not.
   2. `title` is hover-only, appears after a delay, cannot be opened from the
      keyboard, and vanishes on the smallest mouse drift -- all of which is
      wrong for an audience averaging 60-65.
   3. It is NOT modal chrome (DESIGN.md §7): nothing is blocked behind it, Esc
      closes it, and clicking anywhere outside light-dismisses it.

   Its entry/exit motion is defined in the motion block at the end of this
   file, and is gated behind prefers-reduced-motion. */
.info {
  /* the header is 16px; the button must not inherit the 48px control height.
     16px, not the 14px an icon this size invites: U19's floor has exactly ONE
     documented exception (the chart SVG) and this is not going to be the
     second. The glyph fits a 20px dot at 16px, so there is nothing to trade.

     inline-flex, NOT line-height centring. A lowercase "i" is a short stem
     plus a detached dot, so its glyph box sits high in the em box and
     `line-height: 1` leaves it visibly above centre. Flex centring works on
     the glyph box. The italic is gone too: it slanted the stem off the
     circle's vertical axis, which is what made the dot look wrong. */
  display: inline-flex; align-items: center; justify-content: center;
  font: 600 16px/1 "Inter", -apple-system, sans-serif;
  min-height: 0; width: 20px; height: 20px; padding: 0; margin-left: var(--s1);
  border-radius: 50%; border: 1px solid var(--line-strong);
  background: var(--card); color: var(--muted);
  vertical-align: middle; cursor: pointer; position: relative;
  anchor-name: --info-from-comps;
}
/* The visible dot stays 20px because the column pays for every pixel of it,
   but 20px is a small target for this audience. This pseudo-element extends
   the CLICKABLE area to 36px without occupying any layout width. */
.info::after { content: ""; position: absolute; inset: -8px; border-radius: 50%; }
.info:hover { background: var(--accent-wash); border-color: var(--accent);
              color: var(--accent); }
.info:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px;
                      box-shadow: none; }

@supports selector(:popover-open) {
  .pop {
    /* the UA centres an open popover in the viewport; these are the card */
    width: min(28rem, calc(100vw - var(--s6)));
    border: 1px solid var(--line); border-radius: var(--r3);
    background: var(--card); color: var(--ink);
    padding: var(--s5); box-shadow: 0 8px 28px rgba(20, 24, 31, .16);
  }
  /* Tether the card to the "i" it belongs to, rather than letting the UA
     centre it in the viewport where it reads as an unrelated dialog. The
     fallbacks flip it above / to the other side when it would leave the
     viewport, so it stays attached at any scroll position or window size.
     The connection is carried by POSITION; the 4px of entry travel in the
     motion block at the end of this file only reinforces it. */
  @supports (anchor-name: --a) and (position-area: block-end) {
    .pop {
      position: fixed; inset: auto; margin: 0;
      position-anchor: --info-from-comps;
      position-area: block-end span-inline-start;
      margin-block-start: var(--s2);
      position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
    }
    /* No caret. It would have to sit on the top edge when the card opens below
       the dot and on the bottom edge when a fallback flips it above, and CSS
       gives no selector for "which position-try fallback was applied" -- so a
       caret is correct exactly half the time. An 8px gap off the anchor already
       reads as belonging to it. */
  }
  /* No dimmed ::backdrop on purpose. A scrim is what makes a popover READ as a
     modal, and DESIGN.md §7 rules out modal chrome; the card's shadow is enough
     separation. The backdrop still exists and still light-dismisses on click,
     it is simply not painted. */
  .pop::backdrop { background: transparent; }
  .pop h4 { margin: 0 0 var(--s3); font-size: 18px; }
  .pop p { margin: 0 0 var(--s3); font-size: 16px; }
}
/* Fallback: no popover support means the div is an ordinary block, so it reads
   as the explanatory paragraph that used to sit under the table. */
.pop { margin-top: var(--s3); color: var(--muted); }
.pop button { margin-top: var(--s1); }
.muted { color: var(--muted); }

.field { border: 1px solid var(--line); border-radius: var(--r2);
         padding: var(--s5); margin-bottom: var(--s3); background: var(--card); }
.field > * + * { margin-top: var(--s2); }
/* 3px, down from 5px. A 1px rule would read as an ordinary panel edge and lose
   the "this one needs your eyes" signal that is the whole point of the state. */
.field.confirm { border-left: 3px solid var(--warn-line); background: var(--warn-bg); }
.field .name { font-weight: 700; }
.field .maps { color: var(--muted); font-size: 16px; }
.field .samples { color: var(--muted); font-size: 16px; font-family: ui-monospace,
                  SFMono-Regular, Menlo, monospace; }
.field .why { font-size: 16px; color: var(--muted); }

/* The URAR Market Exhibits block under the review chart. The caption is meant
   to be selected and pasted into the appraiser's form software, so it is set
   apart from the prose around it rather than styled as prose. No new colours:
   --card-alt and --muted are already gated by tools/check_contrast.py. */
.urar { margin-top: var(--s4); }
.urar > * + * { margin-top: var(--s2); }
.caption-text { border-left: 3px solid var(--line-strong); padding-left: var(--s3);
                font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
                font-size: 16px; line-height: 1.5; }

/* Data-selection guidance on the upload screen. Bare ul/li are otherwise
   styled only inside .legal, so the list needs its own rules here. No new
   colours: it inherits the panel's, which the contrast gate already covers. */
.guide ul { margin: var(--s3) 0 0; padding-left: var(--s5); }
.guide li { margin-bottom: var(--s2); }
.guide li:last-child { margin-bottom: 0; }

.warn { background: var(--warn-bg); border: 1px solid rgba(217, 145, 0, .35);
        border-left: 3px solid var(--warn-line);
        padding: var(--s3) var(--s4); margin: var(--s3) 0;
        border-radius: 0 var(--r2) var(--r2) 0; }
/* The htmx failure notice (ADR 0047). Reuses the warn palette, which
   `tools/check_contrast.py` already holds to its floor, and 17px because this
   reports that something the appraiser did was NOT done -- the audit refuses
   anything under 16px and this is not the place to sit on the line. */
.alert { margin: 0 0 var(--s4); padding: var(--s3) var(--s4);
         background: var(--warn-bg); border: 1px solid rgba(217, 145, 0, .35);
         border-radius: var(--radius); color: var(--warn-ink); font-size: 17px; }
.alert[hidden] { display: none; }

.warn > * + * { margin-top: var(--s2); }
/* A refusal message quotes back what the appraiser typed (ADR 0043), so this
   box is the one place on the page where an unbroken 200-character run can
   arrive from a paste. Measured: without this the page scrolls to 1972 at
   1280 wide, and `tools/audit_layout.py`'s S3 refused screen is fixtured to
   that length so the guard is exercised rather than assumed. */
.warn p { overflow-wrap: anywhere; }
.warn .code { font-size: 16px; letter-spacing: .3px; color: var(--warn-ink);
              font-weight: 700; }
.rejected { background: var(--card); border: 1px solid var(--line);
            border-left: 3px solid var(--muted);
            padding: var(--s3) var(--s4); margin: var(--s3) 0;
            border-radius: 0 var(--r2) var(--r2) 0; }

/* minmax(0, 1fr), not 1fr: a grid track's automatic minimum is its content's
   min-content width, so a wide table in the left column silently pushes the
   summary rail past the right edge of the page instead of being constrained by
   it. The 0 minimum is what keeps the page from scrolling horizontally. */
.grid2 { display: grid; grid-template-columns: minmax(0, 1fr) 328px;
         gap: var(--s5); align-items: start; }
@media (max-width: 980px) { .grid2 { grid-template-columns: 1fr; } }
.panel { background: var(--card); border: 1px solid var(--line);
         border-radius: var(--r3); padding: var(--s5);
         box-shadow: var(--shadow-sm); }
.panel > :first-child { margin-top: 0; }
.panel > :last-child { margin-bottom: 0; }
/* flex-wrap plus a nowrap value: when the label and its value no longer fit on
   one line the VALUE drops whole to the next line, rather than the date range
   itself breaking across two ("2023-07-07 →" / "2025-04-28" reads as two
   values). Same reason td.date and td.num are nowrap. */
.kv { display: flex; justify-content: space-between; gap: var(--s2) var(--s4);
      flex-wrap: wrap; padding: var(--s2) 0;
      border-bottom: 1px solid var(--line-strong); font-size: 16px; }
.kv:last-child { border-bottom: 0; padding-bottom: 0; }
.kv:first-child { padding-top: 0; }
.kv .k { color: var(--muted); }
.kv .k { flex: 0 1 auto; }
.kv .v { font-variant-numeric: tabular-nums; font-weight: 600;
         text-align: right; white-space: nowrap; margin-left: auto; }
/* The one exception to nowrap: a value with no length limit and no space to
   break at, such as the signed-in email on /account. A 39-character address
   pushed that page to 447px on a 375px phone. `anywhere`, not `break-word`,
   because only `anywhere` lowers the min-content width a flex item sizes to. */
.kv .v.v-wrap { white-space: normal; overflow-wrap: anywhere; min-width: 0; }

.chart { display: block; background: var(--card); border: 1px solid var(--line);
         border-radius: var(--r3); box-shadow: var(--shadow-sm); }
.chart .grid { stroke: #e6eaf0; stroke-width: 1; }
.chart .axis { font-size: 13px; fill: #4a5464;
               font-family: -apple-system, sans-serif; }
.chart .band { fill: #14507d; fill-opacity: .13; }
.chart .fit  { fill: none; stroke: #14507d; stroke-width: 2.5; }
.chart .hit  { fill: transparent; cursor: pointer; }
.chart .hit:focus { outline: 2px solid #14507d; outline-offset: 1px; }
.chart .pt   { fill: #2f6ea8; fill-opacity: .62; pointer-events: none; }
.chart .ptg:hover .pt { fill: #14507d; fill-opacity: 1; r: 7.5; }
.chart .pt.reo { fill: #a8261e; fill-opacity: .7; }
.chart .pt.out { fill: #fff; stroke: #98a2b3; stroke-width: 1.5; }
.chart .comp { stroke: #17603a; stroke-width: 2; stroke-dasharray: 5 4; }
/* 13px and Helvetica, matching the SVG's own attributes in app/chart.py.
   WeasyPrint honours the attribute and the browser honours this rule, so any
   mismatch makes the two renderers disagree -- and the callout box is sized
   from the attribute, so the text overflowed it on screen only. It was 13px
   here against 12px there (8.3%), and -apple-system here against Helvetica
   there, which is a second set of metrics again. The chain's whole premise is
   that one SVG draws identically in both. */
.chart .complab { font-size: 13px; fill: #17603a; font-weight: 600;
                  font-family: Helvetica, Arial, sans-serif; }
.legend { font-size: 16px; color: var(--muted); margin: var(--s3) 0 0; }
.legend span { margin-right: var(--s5); white-space: nowrap; }
.dot { display: inline-block; width: 11px; height: 11px; border-radius: 50%;
       vertical-align: -1px; margin-right: var(--s1); }
.htmx-request .chart { opacity: .55; }

/* --- motion ---------------------------------------------------------------
   DESIGN.md §7 said "no animation" in a list, with no rationale stated there
   or in any ADR. ADR 0017's third amendment revisits it. The replacement is a
   BOUNDED policy rather than a ban, because a ban was already costing
   something: `.htmx-request .chart` snaps to 55% and back on every point
   toggle, which reads as a flicker rather than as "recomputing".

   The rules, and they are the whole policy:

   1. Everything is inside `prefers-reduced-motion: no-preference`, so the
      reader's OS setting wins and reduced-motion users get exactly the
      instant behaviour that shipped before. This is one of the three ways
      WCAG 2.3.3 can be satisfied.
   2. Nothing exceeds --motion (140ms). At this length motion is read as
      causality, not as decoration.
   3. Only `opacity`, `transform`, and colour. NEVER a layout property --
      width, height, margin, padding, or font-size -- so motion can never
      reflow a row or move a number a reader is trying to land on.
   4. Transforms are capped at a few pixels of translate. No parallax, no
      zoom, no spin, no scale: those are the effects that actually trigger
      vestibular symptoms, and the audience here averages 60-65.
   5. **DATA NEVER MOVES.** The fitted curve, the CI band, the plotted points
      and every number in the tables redraw instantly. An appraiser toggling a
      comparable is comparing a before and an after; animating that comparison
      would put a moving target between them and the thing they are checking. */
:root { --motion-fast: 90ms; --motion: 140ms; --ease: cubic-bezier(.2, 0, .2, 1); }

@media (prefers-reduced-motion: no-preference) {
  a, button, .btn, .info {
    transition: background-color var(--motion-fast) var(--ease),
                border-color var(--motion-fast) var(--ease),
                color var(--motion-fast) var(--ease);
  }
  input, select, textarea {
    transition: border-color var(--motion-fast) var(--ease),
                box-shadow var(--motion-fast) var(--ease);
  }
  tr td { transition: background-color var(--motion-fast) var(--ease); }

  /* the one piece of state feedback that was actually missing */
  .chart { transition: opacity var(--motion) var(--ease); }

  /* Popover entry and exit. `allow-discrete` plus @starting-style is what
     lets a display:none -> block element transition at all; without them the
     card would pop in and only fade out. 4px of travel, nothing more. */
  .pop { transition: opacity var(--motion) var(--ease),
                     transform var(--motion) var(--ease); }
  /* Entry only, and deliberately so. The obvious version also sets
     `.pop:not(:popover-open) { opacity: 0 }` to get an exit fade -- but then
     the card's visibility depends on a transition COMPLETING, and if one never
     runs (a frozen tab, a compositor that never gets a frame) the element is
     `:popover-open` with computed opacity 0: present, focusable, and
     invisible. That is a worse bug than having no animation.

     Driving entry purely from @starting-style means the resting cascade value
     is opacity 1, so the failure mode is "it appeared without a fade" rather
     than "it did not appear". The UA's `display: none` already hides it when
     closed, so the exit needs no opacity of its own. */
  .pop:popover-open { opacity: 1; transform: translateY(0); }
  @starting-style { .pop:popover-open { opacity: 0; transform: translateY(-4px); } }
}

/* ---------------------------------------------------------------- legal
   Long-form prose, which nothing else in this app is. Two rules that are not
   cosmetic: the measure is capped near 70 characters because a 1180px line of
   17px text is genuinely hard to track back from at 60-65, and NOTHING here
   drops below 16px -- small print is exactly the text a reader most needs to
   be able to read, and shrinking it is how a disclosure becomes decoration.
   Colours are the existing tokens only, so tools/check_contrast.py has no new
   pair to weigh. Centred in `main`, which is 1180px: capped and pinned left,
   the column left a third of the page empty on one side only. */
.legal { max-width: 68ch; margin-inline: auto; }
.legal h3 { margin: var(--s6) 0 var(--s3); font-size: 19px; }
.legal p, .legal li { font-size: 17px; line-height: 1.6; }
.legal p { margin: 0 0 var(--s3); }
.legal ul { margin: 0 0 var(--s3); padding-left: var(--s5); }
.legal li { margin-bottom: var(--s2); }
.legal table { margin: var(--s3) 0 var(--s5); }
.legal td { padding: var(--s2) var(--s3); font-size: 16px; }

/* A draft banner. Deliberately loud: terms that look finished but are not are
   worse than no terms, because someone may rely on them. */
.notice { background: var(--warn-bg); border: 1px solid rgba(217, 145, 0, .35);
  border-radius: var(--r2); padding: var(--s3) var(--s4);
  color: var(--warn-ink); font-size: 17px; margin: 0 0 var(--s5); }

/* Consent checkbox. The box and its label sit on one baseline, and the whole
   label is the hit target -- a 60-year-old on a deadline should not have to
   hit a 13px square. */
/* Exactly two flex children: the box, and ONE span holding all the text.
   Without the span every inline child -- each link, each run of text --
   becomes its own flex item and the sentence lays out as columns. Neither the
   layout audit nor the contrast gate can see that; a screenshot can. */
.consent label { display: flex; gap: var(--s3); align-items: flex-start;
  font-size: 16px; line-height: 1.55; cursor: pointer; max-width: 62ch; }
.consent label > span { flex: 1 1 auto; }
.consent input[type=checkbox] { width: 24px; height: 24px; margin-top: 1px;
  flex: 0 0 auto; accent-color: var(--accent); }  /* WCAG 2.5.8 floor */

/* Account screens: signup, sign in, forgot, reset, verify (ADR 0075). One
   centred column with ONE card holding every field, rather than a bordered
   `.field` card per input left-aligned in a 1180px page. 480px is wide enough
   for the consent sentence at 16px and narrow enough that a 48px input at
   full width reads as a form, not a banner. The heading and the links under
   the card centre on the column; text INSIDE the card stays left-aligned,
   because centred multi-line copy is harder to read for this audience. */
.auth { max-width: 480px; margin: var(--s6) auto 0; }
.auth > h2, .auth > .lede, .auth-alt { text-align: center; }
.auth > h2 { margin-bottom: var(--s2); }
.auth > .warn, .auth > .panel { margin: 0 0 var(--s4); }
.auth > h2 + .warn, .auth > h2 + .panel, .auth > h2 + .auth-card { margin-top: var(--s5); }
.auth-card { background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r3); padding: var(--s6); box-shadow: var(--shadow-sm);
  display: flex; flex-direction: column; gap: var(--s5); }
.auth-card p { margin: 0; }
.auth-field { display: flex; flex-direction: column; gap: var(--s2); }
.auth-field label { font-weight: 600; }
/* Full width of the card. `min-width: 0` lifts the 260px default above,
   which would otherwise overflow the card at phone width. `.auth` is in the
   selector for SPECIFICITY: that default is `input[type=password]:not([size])`
   (0,2,1), which a bare `.auth-field input` (0,1,1) loses to. */
.auth .auth-field input { width: 100%; min-width: 0; }
.auth-note { font-size: 16px; color: var(--muted); }
.auth-card button[type=submit] { width: 100%; }
/* Password field with its show/hide toggle beside it, both at --control-h.
   The min-width stops the button changing width between "Show" and "Hide",
   which would reflow the input under the pointer. */
.pw { display: flex; gap: var(--s2); }
.auth .pw input { flex: 1 1 0; width: auto; }
.pw-toggle { flex: 0 0 auto; min-width: 88px; padding-left: var(--s3);
  padding-right: var(--s3); }
/* `button { display: inline-block }` above beats the UA's `[hidden]` rule, so
   without this a no-script visitor would see a toggle that does nothing. */
.pw-toggle[hidden] { display: none; }
.auth-alt { margin: var(--s5) 0 0; font-size: 16px; color: var(--muted); }
@media (max-width: 520px) {
  .auth { margin-top: 0; }
  .auth-card { padding: var(--s5); }
}

/* Auto-renewal terms, adjacent to the button that starts them. California's
   ARL wants the disclosure in "visual proximity" to the consent, so this is
   not decoration and must not be moved away from the button or shrunk. */
.renewal { max-width: 60ch; font-size: 16px; color: var(--ink);
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r2); padding: var(--s3) var(--s4); margin: 0 0 var(--s3); }

footer.site { max-width: 1180px; margin: 0 auto;
  padding: var(--s5) var(--s6) var(--s6); border-top: 1px solid var(--line); }
footer.site nav { display: flex; flex-wrap: wrap; gap: var(--s4);
  justify-content: center; }
footer.site a { font-size: 16px; color: var(--muted); }
footer.site a:hover { color: var(--accent); }

/* ---------------------------------------------------- chart hover card
   Replaces the SVG <title> the chart used to carry. That was the browser's
   own tooltip: unstyleable, and gated behind an appearance delay that is a
   browser setting rather than a CSS property. This one is ours, so it is
   instant and it can be made to look like part of the product.

   `transform` rather than `top`/`left` for position: it keeps the card on the
   compositor, so following the cursor costs no layout.

   Text is 16px and 17px. The floor applies here like everywhere else -- a card
   that only appears for a moment is exactly where small type does the most
   damage, because there is no time to lean in. */
.tipcard {
  position: fixed; top: 0; left: 0; z-index: 60;
  pointer-events: none;                 /* never steal the click that excludes */
  display: flex; flex-direction: column; gap: var(--s1);
  padding: var(--s3) var(--s4);
  max-width: 22rem;
  background: var(--tip-bg); color: var(--tip-ink);
  border-radius: var(--r2);
  box-shadow: var(--shadow-lg);
  opacity: 0; visibility: hidden;
  /* Opacity only, and 90ms of it. The ask was "quicker": there is no entry
     delay at all, and the fade is short enough to read as immediate while
     still avoiding the hard flicker of no transition when crossing points. */
  transition: opacity 90ms ease-out, visibility 0s linear 90ms;
}
.tipcard.on { opacity: 1; visibility: visible; transition-delay: 0s; }
.tip-addr { font-size: 17px; font-weight: 600; line-height: 1.3;
            letter-spacing: -.01em; }
.tip-meta { display: flex; align-items: baseline; gap: var(--s3);
            font-size: 16px; color: var(--tip-muted); }
.tip-price { color: var(--tip-ink); font-weight: 600;
             font-variant-numeric: tabular-nums; }
.tip-note { font-size: 16px; color: var(--tip-muted); font-style: italic; }

@media (prefers-reduced-motion: reduce) {
  .tipcard { transition: none; }
}
