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.
251 lines
10 KiB
CSS
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; }
|
|
}
|