Files
cali_scraper/scraper/dashboard/static/style.css
T
kfj001 03eeb6bb62 feat(dashboard): add forward "what's next" signal + fix upcoming floor
The daily-file bill queue is a backward snapshot, so "upcoming" could only
resurface the latest past day (the 08-31 snapshot read as today's queue) and
there was no "what is scheduled next." leginfo bill status/history are
backward-only, so the genuine future-dated signal is the chambers' "upcoming
events" calendars.

- api.py: floor the "upcoming" view at *today* (not the DB max date) when no
  as_of is pinned; surface next_legislative_day + scheduled_events (next 7
  days) on /bills. Add _today_iso / _next_legislative_day / _scheduled_events,
  all degrading to a recess marker / [] (never 5xx).
- relational.py: schema v2 adds the scheduled_events table +
  store_scheduled_events (idempotent per-day refresh), load_scheduled_events,
  next_legislative_day.
- parsers.py: parse_events_page + classify_event_kind for the chamber events
  calendar; skip empty-title rows (decompose the <time> before the fallback).
- ingest.py + cli.py: --store-events lane (cache-first, recess-safe) that
  parses the calendar HTML into scheduled_events.
- sources.py + config.yaml: register assembly_events / senate_events
  (kind: events, empty_is_ok: true).
- run_ingest.sh: run the events lane after the daily-file lane as non-fatal
  (skippable via CALI_SCRAPER_SKIP_EVENTS=1); daily-file exit still gates cron.
- frontend: "Next legislative day" line + "What's next · next 7 days" band
  with honest recess empty states.
- DASHBOARD.md: document the forward signal + known limitations (leginfo
  backward-only gap, recess empty state, 08-31 snapshot is analysis-only).
- tests: 5 new API tests + 18 parse-events tests; 170 pass, 92% coverage.
2026-09-06 19:50:02 -07:00

251 lines
10 KiB
CSS

/* ============================================================================
Legislative Dashboard — visual system (DESIGN_DASHBOARD §9).
Government-appropriate, high-contrast, accessible. One accent. No
gradients, glass, emoji, or hero. Rhythm via scale + spacing + rules.
============================================================================ */
:root {
--ink: #1a1a1a;
--muted: #5b5b5b;
--paper: #ffffff;
--surface: #f4f5f7;
--border: #d5d9df;
--accent: #0b5394; /* sober blue, AA on paper */
--accent-ink: #0a3f6f;
--success: #1e7e34; /* watched / on */
--warn: #8a5a00;
--danger: #b3261e;
--asm: #2f5aa7; /* chamber chip tint only */
--sen: #7a3b8f;
--reason-tag: #0b5394; /* 3px bar: tag match */
--reason-leg: #1e7e34; /* 3px bar: legislator match */
--focus-ring: #0b5394;
--radius: 3px;
--gap: 14px;
--mono: ui-monospace, "SF Mono", "Menlo", monospace;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
color: var(--ink);
background: var(--paper);
line-height: 1.5;
font-size: 15px;
}
/* ---- focus: visible 2px ring, never the only signal ---- */
:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 2px;
}
/* ---- topbar ---- */
.topbar {
position: sticky; top: 0; z-index: 10;
display: flex; align-items: center; gap: 18px;
padding: 10px 18px;
background: var(--paper);
border-bottom: 1px solid var(--border);
}
.wordmark { text-decoration: none; color: var(--ink);
font-weight: 700; letter-spacing: .02em; font-size: 15px; }
.wordmark span { color: var(--accent); }
.nav { display: flex; gap: 14px; }
.nav a { color: var(--muted); text-decoration: none; font-size: 14px;
padding: 6px 2px; border-bottom: 2px solid transparent; }
.nav a:hover { color: var(--ink); }
.nav a.active { color: var(--accent); border-bottom-color: var(--accent); }
.user-band { display: flex; align-items: center; gap: 12px;
margin-left: auto; font-size: 13px; color: var(--muted); }
.user-band button { font-size: 13px; }
.anon-cta { margin-left: auto; text-align: right; }
.anon-cta a { color: var(--accent); font-weight: 600; }
.cta-value { display: block; color: var(--muted); font-size: 12px; }
/* ---- freshness / status band ---- */
.freshness {
display: flex; align-items: center; gap: 10px;
padding: 8px 18px; background: var(--surface);
border-bottom: 1px solid var(--border); font-size: 13px;
color: var(--muted);
}
.badge { background: var(--paper); border: 1px solid var(--border);
padding: 1px 8px; border-radius: 10px; }
.note { color: var(--warn); }
/* ---- controls ---- */
.controls {
display: flex; flex-wrap: wrap; align-items: center; gap: 12px;
padding: 12px 18px; border-bottom: 1px solid var(--border);
font-size: 13px;
}
.controls input, .controls select {
font: inherit; font-size: 13px; padding: 6px 8px;
border: 1px solid var(--border); border-radius: var(--radius);
background: var(--paper); color: var(--ink);
}
.controls .search { min-width: 240px; flex: 1 1 auto; }
.controls label { display: inline-flex; align-items: center; gap: 5px;
color: var(--muted); }
/* ---- layout: main + aside ---- */
main { max-width: 1100px; margin: 0 auto; }
main .controls, main .freshness { max-width: none; }
.wrap-grid { display: grid; grid-template-columns: 1fr 360px;
gap: 20px; padding: 16px 18px; }
@media (max-width: 860px) {
.wrap-grid { grid-template-columns: 1fr; }
}
/* ---- personal controls ---- */
.personal-controls {
padding: 14px 18px; border-bottom: 1px solid var(--border);
background: var(--surface);
}
.zip-lookup { display: flex; align-items: end; gap: 10px; flex-wrap: wrap; }
.zip-lookup label { display: block; font-size: 12px; color: var(--muted);
margin-bottom: 3px; }
.zip-lookup input { font: inherit; padding: 8px 10px; width: 160px;
border: 1px solid var(--border); border-radius: var(--radius); }
.resolve-result { margin-top: 10px; }
.candidate { display: flex; align-items: center; gap: 8px;
padding: 6px 8px; border: 1px solid var(--border);
border-radius: var(--radius); margin: 4px 0; background: var(--paper); }
.candidate .who { font-weight: 600; }
.candidate .dc { color: var(--muted); font-size: 12px; }
.warning { color: var(--danger); font-size: 13px; margin: 6px 0; }
.watch-list, .topic-list {
display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.watch-item, .topic-chip {
display: inline-flex; align-items: center; gap: 6px;
padding: 4px 10px; border: 1px solid var(--border);
border-radius: 14px; font-size: 13px; background: var(--paper);
}
.watch-item.on, .topic-chip.on {
border-color: var(--success); color: var(--success);
background: #eef7f0; }
.topic-chip .count { color: var(--muted); font-size: 11px; }
button.chip-toggle { min-width: 120px; }
/* ---- bands ---- */
.band { padding: 16px 18px; border-bottom: 1px solid var(--border); }
.band-title { font-size: 14px; text-transform: uppercase; letter-spacing:
0.06em; color: var(--muted); margin: 0 0 10px; }
.band-title .count { color: var(--ink); font-weight: 700; }
#personal-band .band-title { color: var(--accent); }
/* ---- forward "what's next" band (scheduled_events) ---- */
.next-band { background: #fbfcff; }
.next-band .band-title { color: var(--accent); }
.ev-row {
display: grid; grid-template-columns: 120px 1fr auto;
gap: 10px; align-items: baseline;
padding: 8px 8px 8px 0; border-bottom: 1px solid var(--border);
}
.ev-date { color: var(--muted); font-size: 13px; white-space: nowrap; }
.ev-main { font-weight: 600; }
.chip.ev-kind { background: #eef3fb; color: var(--asm); border-color: #cdd9ee; }
#freshness-next-day { white-space: nowrap; }
.day-heading {
font-size: 13px; color: var(--muted); margin: 14px 0 6px;
padding-bottom: 4px; border-bottom: 1px solid var(--border); }
.day-heading b { color: var(--ink); }
/* ---- bill row ---- */
.row {
display: grid; grid-template-columns: 76px 1fr auto;
gap: 10px; align-items: start;
padding: 10px 10px 10px 0; border-bottom: 1px solid var(--border);
border-left: 3px solid transparent; padding-left: 8px;
}
.row.relevant { background: #f7fafd; }
.row.relevant.tag-match { border-left-color: var(--reason-tag); }
.row.relevant.leg-match { border-left-color: var(--reason-leg); }
.row.relevant.both { border-left-color: var(--accent); }
.row .title { font-weight: 600; }
.row .meta { color: var(--muted); font-size: 13px; margin-top: 2px; }
.row .num { font-family: var(--mono); font-size: 13px; white-space: nowrap; }
.row .why { color: var(--accent); font-size: 12px; cursor: pointer; }
.row .reason { font-size: 12px; color: var(--muted); margin-top: 4px;
display: none; }
.row .reason.show { display: block; }
.chip { display: inline-block; font-size: 11px; padding: 1px 7px;
border-radius: 10px; border: 1px solid var(--border);
margin-right: 5px; }
.chip.asm { background: #eef3fb; color: var(--asm); border-color:#cdd9ee; }
.chip.sen { background: #f5eef9; color: var(--sen); border-color:#e2d2ea; }
.chip.marker { background: var(--accent); color: #fff; border-color:
var(--accent); }
/* ---- legislative status / "why is this here" reason (always on) ---- */
.row-reason {
margin: 4px 0; font-size: 12px; color: var(--muted);
display: flex; align-items: baseline; gap: 6px; }
.chip.status { border-color: var(--border); }
.chip.status.open { background: #eef3fb; color: var(--asm);
border-color: #cdd9ee; }
.chip.status.closed { background: #f0f0f0; color: var(--warn);
border-color: #d8c9a8; }
.chip.status.pclosed { font-style: italic; }
.row-reason-why { color: var(--muted); }
/* a terminal/closed bill is dimmed so it doesn't read as an open upcoming */
.row-reason[data-closed='1'] { opacity: .85; }
.row-reason[data-closed='1'] .row-reason-why { color: var(--muted); }
.tag { display: inline-block; font-size: 11px; color: var(--muted);
border: 1px solid var(--border); border-radius: 12px;
padding: 0 6px; margin: 0 4px 0 0; }
.row a { color: var(--accent); text-decoration: none; }
.row a:hover { text-decoration: underline; }
/* ---- empty states ---- */
.empty-state { padding: 22px; text-align: center; color: var(--muted);
border: 1px dashed var(--border); border-radius: var(--radius);
background: var(--surface); }
.empty-state h3 { color: var(--ink); margin-top: 0; }
.empty-state input { margin: 8px 0; padding: 8px; font: inherit;
border: 1px solid var(--border); border-radius: var(--radius); }
.escape { color: var(--accent); cursor: pointer; }
/* ---- aside ---- */
.aside { padding: 16px 0; font-size: 13px; }
.aside h3 { font-size: 13px; text-transform: uppercase; letter-spacing:
0.06em; color: var(--muted); }
.teaser { list-style: none; padding: 0; margin: 0 0 14px; }
.teaser li { padding: 4px 0; }
.teaser .n { color: var(--muted); }
.teaser button { margin-left: 6px; font-size: 12px; }
.unlock { display: block; margin-top: 10px; padding: 10px;
border: 1px solid var(--accent); color: var(--accent);
border-radius: var(--radius); text-decoration: none; }
.unlock:hover { background: #f7fafd; }
/* ---- footer ---- */
.footer { padding: 18px; font-size: 12px; color: var(--muted);
border-top: 1px solid var(--border); }
.footer a { color: var(--muted); }
/* ---- buttons ---- */
button { font: inherit; font-size: 13px; padding: 6px 12px;
border: 1px solid var(--border); border-radius: var(--radius);
background: var(--paper); color: var(--ink); cursor: pointer; }
button:hover { background: var(--surface); }
button.ghost { background: transparent; border-color: var(--border);
color: var(--muted); }
button.primary { background: var(--accent); color: #fff;
border-color: var(--accent); }
button.on { background: var(--success); color: #fff;
border-color: var(--success); }
/* motion: minimal. a floated row may fade in once. */
@keyframes fadein { from { opacity: 0; } to { opacity: 1; } }
.row.relevant { animation: fadein 140ms ease-out; }
@media (prefers-reduced-motion: reduce) {
.row.relevant { animation: none; }
}