diff --git a/docs/plans/briefs/web-dashboard-v2/handoff-step3.md b/docs/plans/briefs/web-dashboard-v2/handoff-step3.md new file mode 100644 index 0000000..6a46563 --- /dev/null +++ b/docs/plans/briefs/web-dashboard-v2/handoff-step3.md @@ -0,0 +1,132 @@ +# Step 3 handoff — dashboard restyle + +Branch `v2-dashboard`, worktree `/home/thallada/workspace/the-daily-epub-v2c`, one commit +on top of the merged `v2-frontend` work (step 1 + step 2). + +## Landed + +Every template under `src/web/templates/dashboard/` plus the shared partials +`_candidate_row.html`, `_signals_table.html` and `_pagination.html` is restyled with +Tailwind utilities and the component layer step 2 built. `src/web/static/app.css` is +rebuilt and committed; `npm run css:check` is clean. + +### New/changed pieces in `src/web/tailwind.css` + +`@layer base` gained: a disabled state for every control, `summary { cursor:pointer }`, +and a styled `` (webkit + moz pseudo-elements, `--rule` track, `--good` value, +`--down` when `.over`) for the overview budget bars. + +`@layer components` gained the dashboard vocabulary the templates now use: + +| class | what it is | +| --- | --- | +| `.page-head`, `.page-desc`, `.page-actions`, `.page-eyebrow` | the page header on all 13 pages: eyebrow/breadcrumb, `h1`, one-line description, right-aligned actions, hairline under it | +| `.tiles`, `.tile`, `.tile-label`, `.tile-num`, `.tile-delta` | stat tiles (overview, stats, run detail) | +| `.cell-wrap`, `.cell-tight` | long text cells (`message`, `editor_why`, notes, reasons) wrap with `overflow-wrap:anywhere` and a max width; dates/badges stay on one line | +| `.scroll-x.tall` | opt-in `max-h-[70vh] overflow-y-auto`, which is what actually makes the base layer's `thead { sticky top-0 bg-paper }` do something. Applied to the long tables (runs, candidates, articles, ratings, jobs history, users, settings history, stats) and deliberately not to the small tables inside cards | +| `.filters` / `.filter-actions` / `.form-inline` | one inline filter form per page: rule-bounded row, tiny uppercase labels above each control, buttons grouped at the end | +| `.pager`, `.tabs`, `.pagination` | pagination row and the ratings tab strip (accent underline on `.active`) | +| `.setting`, `.setting-label`, `.setting-input`, `.help`, `.default`, `.secret`, `button.reset`, `.save-bar` | settings: two-column rows collapsing to one on phones, monospace keys, disabled env-locked fields with a `locked` badge and a lock note, sticky save bar with a soft top shadow | +| `.disclosure` (`details.disclosure` + `.disclosure-body`) | the framed `
` blocks (ratings "How ratings enter the algorithm", stats "As text", article "Explain") | +| `.sparklines`, `.spark-figure`, `.spark-axis`, `.spark-legend` | sparkline cards; the SVG classes (`.spark`, `.s0`…`.s5`, `.line`) are untouched | +| `.funnel`, `.funnel-cell` | the funnel bar gets a `bg-paper-2` track; `class="funnel"` is unchanged because a test asserts it verbatim | +| `td .rating` / `.picks .rating` overrides | the rating widget compacted for table cells and the overview picks list: prompt hidden, `min-h-8` buttons, small note field, capped width | +| `tr.superseded` | muted row (the class stays exactly `superseded`; a test counts it) | +| `.badge` default | bare `` now has a neutral tint, plus `.badge.restart` / `.badge.warn` | + +`.dashboard` gained a vertical rhythm (`> * + * { mt-4 }`), section headings as tracked +uppercase labels on a rule (`.dashboard > h2`), muted underlined table links, and +`code` chips on `bg-paper-2`. + +### Per page + +- **overview** — five stat tiles (last run, warnings, verdicts this week, unrated picks, + active jobs) above the existing cards; budget rows are now provider + spend + a meter bar; + jobs/picks lists are hairline-separated with ink links. +- **runs** — page header with Stats/Jobs actions, inline status filter with a Reset that + only appears when a filter is set, numeric columns right-aligned, empty-state row. +- **run** — breadcrumb + status badge header, prev/issue/next as buttons in the header, + four tiles (date, duration, cost, warnings), funnel with a track, cards for the report, + empty-state row for candidates. +- **articles / article** — filter form, wrapped title cells; the article page splits its + facts into "Article" and "Provenance" cards, gives the rating widget its own + "Your verdict" section and keeps everything else as tables. +- **ratings** — description as the page header, tab strip, per-tab filter form, the + in-cell rating widget, muted superseded rows. +- **profile** — editor/preview split (`lg:grid-cols-2`, stacked on phones), monospace + textarea, previews as capped `pre.preview` blocks, history table, `.kv` theme list. +- **stats** — window buttons in the header, `summary` rendered as six stat tiles, + sparkline cards, cards, two filterable tables, text output in a disclosure. +- **jobs / job** — job cards now carry a status badge and a "last started" line linking + to that job (see the one backend change below); the job page is breadcrumb + two cards + + a scrolling monospace journal. +- **settings / settings history / users** — as described in the table above. + +### Backend changes (three, all small) + +1. `JobCard` gained `last_status` / `last_requested` / `last_id`, filled by a new + `attach_last_runs` from the job rows the page already loads (matching `name` or + `name-` for the dated `generate` card). No extra query. Unit-tested. +2. `stats::stats` passed `"dashboard"` as its nav key, so the admin nav highlighted + *Overview* on the Stats page; it now passes `"stats"`. +3. `settings.html` dropped the `! ` prefix in front of the "config.toml on disk does not + load" banner (the `.error` bar already says it); the assertion in + `settings_pages_render_save_and_show_hand_edits` was updated with it. That is the only + test touched. + +## Deviations + +- **`class="inline"` was a latent bug**, the same shape as step 2's `.block` collision: + Tailwind emits an `.inline { display:inline }` utility, which beats `form.inline`'s + `@apply flex` in the components layer, so those forms were laid out inline. The class + is renamed to `form-inline` in `jobs.html`, `profile.html` and `settings.html` and in + `tailwind.css`. Nothing asserted on it. +- **Sticky table heads need a scroll box.** `thead { sticky top-0 }` from step 2 is inert + inside a plain `overflow-x:auto` container, so the long tables opt into + `.scroll-x.tall` (`max-h-[70vh]`). Small tables inside cards keep the plain `.scroll-x`. +- The brief asks for "last run" on the job cards; the data was not in `JobCard`, so it was + added (item 1 above) rather than skipped. +- `_rating_widget.html` itself is untouched — two tests assert its exact attribute order + (`value="loved" data-label="loved" class="active"`), so the table-cell treatment is done + purely with `td .rating …` rules. +- `.dashboard > h2` renders section headings as small tracked uppercase labels on a rule + rather than as large sans headings; `.card h2`/`h3` stay normal-weight `text-base`. + +## Hooks and asserted classes (all preserved) + +`form.rating`, `button[data-label]`, `.active` + `aria-pressed`, `form[data-confirm]`, +`details[id]` (`ratings-how`, `profile-prompt`, plus new `stats-text` and `article-explain`), +`table[data-filter]`, `.table-filter`, `button[data-reset]`/`data-default`, `[data-refresh]`, +`.scroll-x`, `.spark`/`.s0`–`.s5`/`.line`, `class="funnel"`, `class="superseded"`, +`badge `, `

Users

`, every form `action`/`name`/`id` and every `aria-*`. + +## Verification + +- `npm run css` + `npm run css:check` — clean. +- `cargo fmt`, `cargo clippy --all-targets -- -D warnings` — clean. +- `cargo test` — **477 passed, 0 failed** (441 lib + 9 + 2 + 3 + 4 + 7 + 9 + 2 integration, + one binary with 0 tests). Run outside the Codex sandbox, so the socket-binding tests ran. +- Dev server on `127.0.0.1:3601` (port 3599/3602 belong to other agents, 3499 to + production). Every dashboard route returned 200, with **no console errors and no asset + 404s** in the server log. + +### Screenshots + +`/tmp/claude-1000/-home-thallada-workspace-the-daily-epub/e963db53-510f-4312-ac87-460291a92781/scratchpad/step3-shots/` +— 58 PNGs, all signed in as `admin`: `/dashboard`, `/dashboard/runs`, `/dashboard/runs/2`, +`/dashboard/articles`, `/dashboard/articles/1`, `/dashboard/ratings`, +`/dashboard/ratings?tab=events`, `/dashboard/profile`, `/dashboard/stats`, +`/dashboard/jobs`, `/dashboard/jobs/1`, `/dashboard/settings`, +`/dashboard/settings/history`, `/dashboard/users` × {light, dark} × {1280px desktop, +390px mobile}, full page, plus viewport shots of the settings save bar +(`*-top.png`, desktop light/dark and mobile). + +The throwaway `./dev` database was extended by hand for the shoot (four `jobs` rows, eight +`rating_events`, four `config_changes`) so the jobs, ratings and settings-history pages had +content; `dev/` is git-ignored and `examples/seed_dev_db.rs` was **not** changed. A future +design pass may want those rows in the seeder. + +## Left for later + +- Step 4 adds the collapsing table-of-contents sidebar to the signed-in issue pages. +- `examples/seed_dev_db.rs` still seeds no jobs, ratings or config changes. diff --git a/src/web/dashboard/jobs.rs b/src/web/dashboard/jobs.rs index c1cc0a3..0247597 100644 --- a/src/web/dashboard/jobs.rs +++ b/src/web/dashboard/jobs.rs @@ -45,6 +45,12 @@ struct JobCard { /// The `generate` card carries the date input for `generate-YYYY-MM-DD`. dated: bool, lock: Option<&'static str>, + /// Status of the most recent job started from this card, when there is one. + last_status: Option, + /// When that job was requested, for the card's "last run" line. + last_requested: Option, + /// Its id, so the card links straight to the job page. + last_id: Option, } #[derive(Debug, Clone)] @@ -95,10 +101,29 @@ fn cards() -> Vec { dangerous: job.dangerous(), dated: matches!(job, Job::Generate { .. }), lock: job.takes_lock(), + last_status: None, + last_requested: None, + last_id: None, }) .collect() } +/// `rows` is newest first, so the first row whose name is the card's name (or +/// `-` for the dated `generate` card) is that card's last run. +fn attach_last_runs(cards: &mut [JobCard], rows: &[JobLine]) { + for card in cards.iter_mut() { + let prefix = format!("{}-", card.name); + if let Some(row) = rows + .iter() + .find(|row| row.name == card.name || row.name.starts_with(&prefix)) + { + card.last_status = Some(row.status.clone()); + card.last_requested = Some(row.requested.clone()); + card.last_id = Some(row.id); + } + } +} + fn job_line(row: &JobRow, config: &Config) -> JobLine { JobLine { id: row.id, @@ -140,11 +165,14 @@ async fn jobs_template( .date() .to_string() }); + let jobs: Vec = rows.iter().map(|row| job_line(row, &config)).collect(); + let mut cards = cards(); + attach_last_runs(&mut cards, &jobs); Ok(JobsTemplate { page, jobs_enabled: config.server.jobs_enabled, - cards: cards(), - jobs: rows.iter().map(|row| job_line(row, &config)).collect(), + cards, + jobs, today, }) } @@ -352,6 +380,47 @@ mod tests { use crate::web::MockRunner; use crate::web::dashboard::tests::{assert_admin_only, get, login_cookie, response_text}; + fn job_line_named(id: i64, name: &str, status: &str) -> JobLine { + JobLine { + id, + name: name.into(), + requested_by: "admin".into(), + requested: "2026-09-03 01:00".into(), + started: String::new(), + finished: String::new(), + duration: String::new(), + status: status.into(), + message: None, + run_id: None, + } + } + + #[test] + fn cards_take_their_last_run_from_the_newest_matching_job() { + let mut cards = cards(); + // Newest first, the way `jobs::list` returns them. + let rows = vec![ + job_line_named(4, "generate-2026-09-03", "running"), + job_line_named(3, "profile-rebuild", "ok"), + job_line_named(2, "generate-2026-09-02", "ok"), + ]; + attach_last_runs(&mut cards, &rows); + + let generate = cards.iter().find(|card| card.name == "generate").unwrap(); + assert_eq!(generate.last_id, Some(4), "the dated job matches its card"); + assert_eq!(generate.last_status.as_deref(), Some("running")); + let rebuild = cards + .iter() + .find(|card| card.name == "profile-rebuild") + .unwrap(); + assert_eq!(rebuild.last_id, Some(3)); + let never = cards + .iter() + .find(|card| card.last_id.is_none()) + .expect("a catalogue job with no recorded run"); + assert!(never.last_status.is_none()); + } + async fn app_with_runner( config: Config, runner: Arc, diff --git a/src/web/dashboard/settings.rs b/src/web/dashboard/settings.rs index 1d4402a..8a316f2 100644 --- a/src/web/dashboard/settings.rs +++ b/src/web/dashboard/settings.rs @@ -2495,7 +2495,7 @@ mod tests { ) .await; assert!( - page.contains("! config.toml on disk does not load:"), + page.contains("config.toml on disk does not load:"), "{page}" ); assert!(page.contains("value=\"31\""), "{page}"); diff --git a/src/web/dashboard/stats.rs b/src/web/dashboard/stats.rs index 087c1b8..7fc5fa4 100644 --- a/src/web/dashboard/stats.rs +++ b/src/web/dashboard/stats.rs @@ -406,7 +406,7 @@ async fn stats( }) .collect(); - let mut page = Page::new("Stats", viewer, "dashboard"); + let mut page = Page::new("Stats", viewer, "stats"); page.flash = take_flash(&session).await?; Ok(Html(StatsTemplate { page, diff --git a/src/web/static/app.css b/src/web/static/app.css index e619862..e00e3c3 100644 --- a/src/web/static/app.css +++ b/src/web/static/app.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;--font-serif:"Newsreader", "Iowan Old Style", "Palatino Linotype", Georgia, serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, monospace;--spacing:.25rem;--container-md:28rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-medium:500;--font-weight-semibold:600;--tracking-normal:0em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-sm:.25rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;--default-mono-font-family:ui-monospace, SFMono-Regular, Menlo, monospace;--color-paper:var(--paper);--color-paper-2:var(--paper-2);--color-ink:var(--ink)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])),[hidden]{display:none!important}html{background-color:var(--paper);color:var(--ink);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{background-color:var(--paper);--tw-leading:1.65;min-height:100vh;color:var(--ink);font-optical-sizing:auto;margin:0;font-family:Newsreader,Iowan Old Style,Palatino Linotype,Georgia,serif;font-size:1.0625rem;line-height:1.65}::selection{background:var(--accent)}@supports (color:color-mix(in lab, red, red)){::selection{background:color-mix(in oklab, var(--accent) 24%, transparent)}}a{color:var(--accent);-webkit-text-decoration-color:var(--rule);-webkit-text-decoration-color:var(--rule);text-decoration-color:var(--rule);text-underline-offset:4px;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;transition-duration:.15s}@media (hover:hover){a:hover{color:var(--accent-hover)}}a:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--accent);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--paper);--tw-outline-style:none;outline-style:none}h1,h2,h3{color:var(--ink);font-optical-sizing:auto}button,input,select,textarea{font:inherit;border-radius:var(--radius-sm);border-style:var(--tw-border-style);border-width:1px;border-color:var(--rule);background-color:var(--paper);padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * 1.5);color:var(--ink);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;transition-duration:.15s}:is(button,input,select,textarea)::placeholder{color:var(--muted)}:is(button,input,select,textarea):focus{border-color:var(--ink);--tw-outline-style:none;outline-style:none}:is(button,input,select,textarea):focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--accent);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--paper)}button{cursor:pointer}textarea{min-height:calc(var(--spacing) * 28)}table{border-collapse:collapse;width:100%;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:var(--leading-snug);line-height:var(--leading-snug);font-variant-numeric:tabular-nums}thead{z-index:10;background-color:var(--paper);position:sticky;top:0}th,td{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--rule);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 2);text-align:left;vertical-align:top}th{--tw-font-weight:var(--font-weight-semibold);font-size:.72rem;font-weight:var(--font-weight-semibold);--tw-tracking:.1em;letter-spacing:.1em;color:var(--muted);text-transform:uppercase}tbody tr{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;transition-duration:.15s}@media (hover:hover){tbody tr:hover{background-color:var(--paper-2)}}code,pre{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important}}}@layer components{.btn,.button{min-height:calc(var(--spacing) * 10);border-radius:var(--radius-sm);border-style:var(--tw-border-style);border-width:1px;border-color:var(--rule-strong);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--ink);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;background-color:#0000;justify-content:center;align-items:center;text-decoration-line:none;transition-duration:.15s;display:inline-flex}@media (hover:hover){:is(.btn,.button):hover{background-color:var(--ink);color:var(--paper)}}:is(.btn,.button):focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--accent);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--paper);--tw-outline-style:none;outline-style:none}.btn-primary{min-height:calc(var(--spacing) * 10);border-radius:var(--radius-sm);border-style:var(--tw-border-style);border-width:1px;border-color:var(--ink);background-color:var(--ink);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--paper);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;justify-content:center;align-items:center;text-decoration-line:none;transition-duration:.15s;display:inline-flex}@media (hover:hover){.btn-primary:hover{border-color:var(--accent);background-color:var(--accent);color:var(--paper)}}.btn-primary:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--accent);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--paper);--tw-outline-style:none;outline-style:none}.badge{padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * .5);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);border-radius:3.40282e38px;align-items:center;display:inline-flex}.badge.selected,.badge.loved,.badge.ok{color:var(--loved);background:var(--loved)}@supports (color:color-mix(in lab, red, red)){.badge.selected,.badge.loved,.badge.ok{background:color-mix(in oklab, var(--loved) 14%, transparent)}}.badge.good,.badge.assessed,.badge.triaged,.badge.degraded{color:var(--good);background:var(--good)}@supports (color:color-mix(in lab, red, red)){.badge.good,.badge.assessed,.badge.triaged,.badge.degraded{background:color-mix(in oklab, var(--good) 14%, transparent)}}.badge.down,.badge.excluded,.badge.failed,.badge.dry_run{color:var(--down);background:var(--down)}@supports (color:color-mix(in lab, red, red)){.badge.down,.badge.excluded,.badge.failed,.badge.dry_run{background:color-mix(in oklab, var(--down) 14%, transparent)}}.badge.shortlisted,.badge.admitted,.badge.eligible,.badge.cleared,.badge.reason,.badge.running,.badge.requested{color:var(--muted);background:var(--muted)}@supports (color:color-mix(in lab, red, red)){.badge.shortlisted,.badge.admitted,.badge.eligible,.badge.cleared,.badge.reason,.badge.running,.badge.requested{background:color-mix(in oklab, var(--muted) 14%, transparent)}}.kv{column-gap:calc(var(--spacing) * 5);row-gap:var(--spacing);grid-template-columns:minmax(9rem,1fr) 2fr;display:grid}.kv dt{color:var(--muted)}.kv dd{overflow-wrap:anywhere;min-width:0;margin:0}.scroll-x{overscroll-behavior-inline:contain;max-width:100%;overflow-x:auto}.scroll-x>table{min-width:max-content}.prose-body{--tw-leading:1.72;color:var(--ink);font-size:1.0625rem;line-height:1.72}.prose-body>*+*{margin-top:calc(var(--spacing) * 5)}.prose-body h2{margin-top:calc(var(--spacing) * 10);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--rule);padding-top:calc(var(--spacing) * 4);font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height));--tw-leading:1.15;--tw-font-weight:var(--font-weight-semibold);line-height:1.15;font-weight:var(--font-weight-semibold);--tw-tracking:-.01em;letter-spacing:-.01em}.prose-body h3{margin-top:calc(var(--spacing) * 8);font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-leading:var(--leading-tight);line-height:var(--leading-tight);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.prose-body a{color:var(--ink);text-decoration-line:underline;-webkit-text-decoration-color:var(--rule);-webkit-text-decoration-color:var(--rule);text-decoration-color:var(--rule);text-underline-offset:4px}@media (hover:hover){.prose-body a:hover{color:var(--ink);-webkit-text-decoration-color:var(--accent);-webkit-text-decoration-color:var(--accent);text-decoration-color:var(--accent)}}.prose-body img{margin-inline:auto;margin-block:calc(var(--spacing) * 7);border-radius:var(--radius-sm);text-align:center;max-width:100%;height:auto;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--muted);font-style:italic;display:block}.prose-body pre{border-radius:var(--radius-sm);background-color:var(--paper-2);max-width:100%;padding:calc(var(--spacing) * 4);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);overflow-x:auto}.prose-body :not(pre)>code{border-radius:var(--radius-sm);background-color:var(--paper-2);padding-inline:var(--spacing);padding-block:calc(var(--spacing) * .5);font-size:.9em}.prose-body blockquote{margin-block:calc(var(--spacing) * 7);border-left-style:var(--tw-border-style);border-left-width:2px;border-color:var(--rule-strong);padding-left:calc(var(--spacing) * 5);color:var(--ink-2)}.prose-body ul{padding-left:calc(var(--spacing) * 6);list-style-type:disc}.prose-body ol{padding-left:calc(var(--spacing) * 6);list-style-type:decimal}.prose-body li::marker{color:var(--muted)}.prose-body li+li{margin-top:calc(var(--spacing) * 2)}.rating{margin-block:calc(var(--spacing) * 5);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));flex-wrap:wrap;align-items:center;display:flex}.rating-prompt{margin-right:calc(var(--spacing) * 3);--tw-tracking:.1em;letter-spacing:.1em;color:var(--muted);text-transform:uppercase;font-size:.72rem}.rating button[data-label]:not(.clear){min-height:calc(var(--spacing) * 10);border-color:var(--rule);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);color:var(--ink);background-color:#0000;border-radius:0;margin-left:-1px}.rating button[data-label]:not(.clear):first-child{border-top-left-radius:var(--radius-sm);border-bottom-left-radius:var(--radius-sm);margin-left:0}.rating button[data-label]:not(.clear):last-child{border-top-right-radius:var(--radius-sm);border-bottom-right-radius:var(--radius-sm)}@media (hover:hover){.rating button[data-label]:not(.clear):hover{z-index:10;border-color:var(--ink);background-color:var(--paper-2)}}.rating button[data-label]:not(.clear).active{z-index:10;border-color:var(--ink);background-color:var(--ink);color:var(--paper)}.rating button.clear{margin-left:calc(var(--spacing) * 2);min-height:calc(var(--spacing) * 10);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 2);color:var(--muted);text-decoration-line:underline;-webkit-text-decoration-color:var(--rule);-webkit-text-decoration-color:var(--rule);text-decoration-color:var(--rule);text-underline-offset:4px;background-color:#0000;border-width:0}@media (hover:hover){.rating button.clear:hover{color:var(--ink)}}.rating button.clear.active{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--ink)}.rating-note{margin-top:calc(var(--spacing) * 2);align-items:center;gap:calc(var(--spacing) * 2);color:var(--muted);flex-basis:100%;display:flex}.rating-note input{flex:1}.flash{margin-block:calc(var(--spacing) * 4);border-left-style:var(--tw-border-style);border-left-width:4px;border-color:var(--accent);background-color:var(--paper-2);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.error{border-left-style:var(--tw-border-style);border-left-width:4px;border-color:var(--down);background-color:var(--paper-2);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--down)}.notice{border-left-style:var(--tw-border-style);border-left-width:4px;border-color:var(--warn);background-color:var(--paper-2);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--ink-2)}.dashboard{margin-inline:auto;margin-block:calc(var(--spacing) * 8);max-width:var(--container-7xl);padding-inline:calc(var(--spacing) * 4);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-leading:var(--leading-normal);line-height:var(--leading-normal)}@media (min-width:40rem){.dashboard{padding-inline:calc(var(--spacing) * 6)}}.dashboard h1{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.dashboard h2{margin-top:calc(var(--spacing) * 7);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.dashboard h3{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.cards{margin-block:calc(var(--spacing) * 4);gap:calc(var(--spacing) * 4);grid-template-columns:repeat(auto-fit,minmax(min(20rem,100%),1fr));display:grid}.card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--rule);min-width:0;padding:calc(var(--spacing) * 4)}.muted,.meta{color:var(--muted)}.num{text-align:right;white-space:nowrap;--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.filters,form.filters,form.inline{margin-block:calc(var(--spacing) * 4);align-items:flex-end;gap:calc(var(--spacing) * 3);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));flex-wrap:wrap;display:flex}.filters label,form.filters label{gap:var(--spacing);display:grid}.table-filter{margin-block:calc(var(--spacing) * 2);width:calc(var(--spacing) * 80);max-width:100%;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.pager,.run-nav,.tabs{margin-block:calc(var(--spacing) * 4);align-items:center;gap:calc(var(--spacing) * 4);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));flex-wrap:wrap;display:flex}pre.block,pre.preview,pre.journal{border-radius:var(--radius-sm);background-color:var(--paper-2);padding:calc(var(--spacing) * 3);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));white-space:pre-wrap;overflow-wrap:anywhere;overflow:auto}.funnel>*{margin-block:calc(var(--spacing) * .5);background-color:var(--good);min-width:1px}.spark{max-width:100%;height:auto}.spark .s0{fill:var(--good)}.spark .s1{fill:var(--loved)}.spark .s2{fill:var(--down)}.spark .s3{fill:var(--accent)}.spark .s4{fill:var(--muted)}.spark .s5{fill:var(--ink)}.spark .line{stroke:var(--accent)}@media (max-width:40rem){.kv{display:block}.kv dt{margin-top:calc(var(--spacing) * 2)}.rating{align-items:stretch}.rating-prompt,.rating-note{flex-basis:100%}.rating-prompt{margin-bottom:calc(var(--spacing) * 2)}}}@layer utilities{.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.top-1{top:var(--spacing)}.top-3{top:calc(var(--spacing) * 3)}.top-4{top:calc(var(--spacing) * 4)}.top-20{top:calc(var(--spacing) * 20)}.top-40{top:calc(var(--spacing) * 40)}.top-120{top:calc(var(--spacing) * 120)}.left-3{left:calc(var(--spacing) * 3)}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:0}.mx-auto{margin-inline:auto}.my-4{margin-block:calc(var(--spacing) * 4)}.my-8{margin-block:calc(var(--spacing) * 8)}.my-12{margin-block:calc(var(--spacing) * 12)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-7{margin-top:calc(var(--spacing) * 7)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-10{margin-top:calc(var(--spacing) * 10)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mt-16{margin-top:calc(var(--spacing) * 16)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.mb-9{margin-bottom:calc(var(--spacing) * 9)}.mb-10{margin-bottom:calc(var(--spacing) * 10)}.ml-1{margin-left:var(--spacing)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.table{display:table}.h-4{height:calc(var(--spacing) * 4)}.max-h-\[calc\(100vh-2rem\)\]{max-height:calc(100vh - 2rem)}.min-h-8{min-height:calc(var(--spacing) * 8)}.min-h-10{min-height:calc(var(--spacing) * 10)}.min-h-11{min-height:calc(var(--spacing) * 11)}.min-h-24{min-height:calc(var(--spacing) * 24)}.min-h-\[68vh\]{min-height:68vh}.w-4{width:calc(var(--spacing) * 4)}.w-64{width:calc(var(--spacing) * 64)}.w-full{width:100%}.max-w-7xl{max-width:var(--container-7xl)}.max-w-\[58ch\]{max-width:58ch}.max-w-\[68ch\]{max-width:68ch}.max-w-md{max-width:var(--container-md)}.grow{flex-grow:1}.-translate-y-20{--tw-translate-y:calc(var(--spacing) * -20);translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.resize{resize:both}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-\[1fr_auto\]{grid-template-columns:1fr auto}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-y-1{row-gap:var(--spacing)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded-sm{border-radius:var(--radius-sm)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-b-\[3px\]{border-bottom-style:var(--tw-border-style);border-bottom-width:3px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-double{--tw-border-style:double;border-style:double}.border-accent{border-color:var(--accent)}.border-ink{border-color:var(--ink)}.border-rule{border-color:var(--rule)}.border-rule-strong{border-color:var(--rule-strong)}.border-transparent{border-color:#0000}.bg-accent{background-color:var(--accent)}.bg-ink{background-color:var(--ink)}.bg-paper{background-color:var(--paper)}.bg-paper-2{background-color:var(--paper-2)}.bg-transparent{background-color:#0000}.fill-none{fill:none}.stroke-current{stroke:currentColor}.p-0{padding:0}.p-4{padding:calc(var(--spacing) * 4)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-16{padding-block:calc(var(--spacing) * 16)}.py-20{padding-block:calc(var(--spacing) * 20)}.pt-0{padding-top:0}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pt-7{padding-top:calc(var(--spacing) * 7)}.pt-8{padding-top:calc(var(--spacing) * 8)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-9{padding-bottom:calc(var(--spacing) * 9)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-6{padding-left:calc(var(--spacing) * 6)}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.font-sans{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}.font-serif{font-family:Newsreader,Iowan Old Style,Palatino Linotype,Georgia,serif}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.68rem\]{font-size:.68rem}.text-\[0\.72rem\]{font-size:.72rem}.text-\[0\.95rem\]{font-size:.95rem}.leading-\[1\.1\]{--tw-leading:1.1;line-height:1.1}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[-0\.01em\]{--tw-tracking:-.01em;letter-spacing:-.01em}.tracking-\[-0\.02em\]{--tw-tracking:-.02em;letter-spacing:-.02em}.tracking-\[0\.1em\]{--tw-tracking:.1em;letter-spacing:.1em}.tracking-\[0\.08em\]{--tw-tracking:.08em;letter-spacing:.08em}.tracking-\[0\.12em\]{--tw-tracking:.12em;letter-spacing:.12em}.tracking-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.text-accent{color:var(--accent)}.text-down{color:var(--down)}.text-good{color:var(--good)}.text-ink{color:var(--ink)}.text-ink-2{color:var(--ink-2)}.text-loved{color:var(--loved)}.text-muted{color:var(--muted)}.text-paper{color:var(--paper)}.text-warn{color:var(--warn)}.lowercase{text-transform:lowercase}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.decoration-rule{-webkit-text-decoration-color:var(--rule);-webkit-text-decoration-color:var(--rule);text-decoration-color:var(--rule)}.underline-offset-4{text-underline-offset:4px}.shadow,.shadow\/backfill,.shadow\/compatibility,.shadow\/dry,.shadow\/maintenance{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-accent{--tw-ring-color:var(--accent)}.ring-offset-2{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.ring-offset-paper{--tw-ring-offset-color:var(--paper)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.grayscale{--tw-grayscale:grayscale(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.group-hover\:text-accent:is(:where(.group):hover *){color:var(--accent)}.hover\:border-accent:hover{border-color:var(--accent)}.hover\:bg-paper-2:hover{background-color:var(--paper-2)}.hover\:text-accent:hover{color:var(--accent)}.hover\:text-accent-hover:hover{color:var(--accent-hover)}.hover\:text-ink:hover{color:var(--ink)}.hover\:decoration-accent:hover{-webkit-text-decoration-color:var(--accent);-webkit-text-decoration-color:var(--accent);text-decoration-color:var(--accent)}}.focus\:translate-y-0:focus{--tw-translate-y:0px;translate:var(--tw-translate-x) var(--tw-translate-y)}.focus\:border-ink:focus{border-color:var(--ink)}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}@media (min-width:40rem){.sm\:col-start-2{grid-column-start:2}.sm\:mt-12{margin-top:calc(var(--spacing) * 12)}.sm\:mt-14{margin-top:calc(var(--spacing) * 14)}.sm\:mt-16{margin-top:calc(var(--spacing) * 16)}.sm\:inline{display:inline}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}.sm\:py-4{padding-block:calc(var(--spacing) * 4)}.sm\:pt-6{padding-top:calc(var(--spacing) * 6)}.sm\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.sm\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}@media (min-width:48rem){.md\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}}@media (min-width:80rem){.xl\:w-72{width:calc(var(--spacing) * 72)}}.\[\&_\.comment-meta\]\:font-sans .comment-meta{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}.\[\&_\.comment-meta\]\:text-sm .comment-meta{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.\[\&_\.comment-meta\]\:text-muted .comment-meta{color:var(--muted)}.\[\&_blockquote\.reply\]\:ml-6 blockquote.reply{margin-left:calc(var(--spacing) * 6)}.\[\&\>p\:first-child\]\:first-letter\:float-left>p:first-child:first-letter{float:left}.\[\&\>p\:first-child\]\:first-letter\:mt-1>p:first-child:first-letter{margin-top:var(--spacing)}.\[\&\>p\:first-child\]\:first-letter\:mr-2>p:first-child:first-letter{margin-right:calc(var(--spacing) * 2)}.\[\&\>p\:first-child\]\:first-letter\:font-serif>p:first-child:first-letter{font-family:Newsreader,Iowan Old Style,Palatino Linotype,Georgia,serif}.\[\&\>p\:first-child\]\:first-letter\:text-\[4\.6rem\]>p:first-child:first-letter{font-size:4.6rem}.\[\&\>p\:first-child\]\:first-letter\:leading-\[0\.72\]>p:first-child:first-letter{--tw-leading:.72;line-height:.72}.\[\&\>p\:first-child\]\:first-letter\:font-semibold>p:first-child:first-letter{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}}@font-face{font-family:Newsreader;font-style:normal;font-weight:200 800;font-stretch:100%;font-display:swap;src:url(/static/Newsreader.woff2)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Newsreader;font-style:italic;font-weight:200 800;font-stretch:100%;font-display:swap;src:url(/static/Newsreader-italic.woff2)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--paper:#f6f3ec;--paper-2:#ece7db;--ink:#1c1b18;--ink-2:#4f4b43;--muted:#7a7568;--rule:#d8d2c4;--rule-strong:#1c1b18;--accent:#a3231f;--accent-hover:#7c1a16;--loved:#2f6f46;--good:#2f6a8f;--down:#9c3f36;--warn:#8a6d1f;color-scheme:light}@media (prefers-color-scheme:dark){:root:not([data-theme=light]){--paper:#151513;--paper-2:#1e1d1a;--ink:#ebe6da;--ink-2:#bfb9aa;--muted:#8f8a7d;--rule:#3a3833;--rule-strong:#ebe6da;--accent:#ef8c82;--accent-hover:#f7aaa2;--loved:#7fcb92;--good:#7fb8d8;--down:#ea8f84;--warn:#d9b45a;color-scheme:dark}}:root[data-theme=dark]{--paper:#151513;--paper-2:#1e1d1a;--ink:#ebe6da;--ink-2:#bfb9aa;--muted:#8f8a7d;--rule:#3a3833;--rule-strong:#ebe6da;--accent:#ef8c82;--accent-hover:#f7aaa2;--loved:#7fcb92;--good:#7fb8d8;--down:#ea8f84;--warn:#d9b45a;color-scheme:dark}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false} \ No newline at end of file +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;--font-serif:"Newsreader", "Iowan Old Style", "Palatino Linotype", Georgia, serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, monospace;--spacing:.25rem;--container-md:28rem;--container-lg:32rem;--container-2xl:42rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--tracking-normal:0em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-sm:.25rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;--default-mono-font-family:ui-monospace, SFMono-Regular, Menlo, monospace;--color-paper:var(--paper);--color-paper-2:var(--paper-2);--color-ink:var(--ink)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])),[hidden]{display:none!important}html{background-color:var(--paper);color:var(--ink);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{background-color:var(--paper);--tw-leading:1.65;min-height:100vh;color:var(--ink);font-optical-sizing:auto;margin:0;font-family:Newsreader,Iowan Old Style,Palatino Linotype,Georgia,serif;font-size:1.0625rem;line-height:1.65}::selection{background:var(--accent)}@supports (color:color-mix(in lab, red, red)){::selection{background:color-mix(in oklab, var(--accent) 24%, transparent)}}a{color:var(--accent);-webkit-text-decoration-color:var(--rule);-webkit-text-decoration-color:var(--rule);text-decoration-color:var(--rule);text-underline-offset:4px;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;transition-duration:.15s}@media (hover:hover){a:hover{color:var(--accent-hover)}}a:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--accent);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--paper);--tw-outline-style:none;outline-style:none}h1,h2,h3{color:var(--ink);font-optical-sizing:auto}button,input,select,textarea{font:inherit;border-radius:var(--radius-sm);border-style:var(--tw-border-style);border-width:1px;border-color:var(--rule);background-color:var(--paper);padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * 1.5);color:var(--ink);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;transition-duration:.15s}:is(button,input,select,textarea)::placeholder{color:var(--muted)}:is(button,input,select,textarea):focus{border-color:var(--ink);--tw-outline-style:none;outline-style:none}:is(button,input,select,textarea):focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--accent);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--paper)}button{cursor:pointer}button:disabled,input:disabled,select:disabled,textarea:disabled{cursor:not-allowed;border-color:var(--rule);background-color:var(--paper-2);color:var(--muted)}textarea{min-height:calc(var(--spacing) * 28)}summary{cursor:pointer}table{border-collapse:collapse;width:100%;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:var(--leading-snug);line-height:var(--leading-snug);font-variant-numeric:tabular-nums}thead{z-index:10;background-color:var(--paper);position:sticky;top:0}th,td{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--rule);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 2);text-align:left;vertical-align:top}th{--tw-font-weight:var(--font-weight-semibold);font-size:.72rem;font-weight:var(--font-weight-semibold);--tw-tracking:.1em;letter-spacing:.1em;color:var(--muted);text-transform:uppercase}tbody tr{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;transition-duration:.15s}@media (hover:hover){tbody tr:hover{background-color:var(--paper-2)}}code,pre{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}meter{height:calc(var(--spacing) * 2);border-radius:var(--radius-sm);appearance:none;background:var(--rule);width:100%;display:block}meter::-webkit-meter-bar{background:var(--rule);border:0;border-radius:2px;height:.5rem}meter::-webkit-meter-optimum-value{background:var(--good);border-radius:2px}meter::-webkit-meter-suboptimum-value{background:var(--good);border-radius:2px}meter::-webkit-meter-even-less-good-value{background:var(--good);border-radius:2px}meter.over::-webkit-meter-optimum-value{background:var(--down)}meter.over::-webkit-meter-suboptimum-value{background:var(--down)}meter.over::-webkit-meter-even-less-good-value{background:var(--down)}meter::-moz-meter-bar{background:var(--good);border-radius:2px}meter.over::-moz-meter-bar{background:var(--down)}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important}}}@layer components{.btn,.button{min-height:calc(var(--spacing) * 10);border-radius:var(--radius-sm);border-style:var(--tw-border-style);border-width:1px;border-color:var(--rule-strong);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--ink);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;background-color:#0000;justify-content:center;align-items:center;text-decoration-line:none;transition-duration:.15s;display:inline-flex}@media (hover:hover){:is(.btn,.button):hover{background-color:var(--ink);color:var(--paper)}}:is(.btn,.button):focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--accent);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--paper);--tw-outline-style:none;outline-style:none}.btn-primary{min-height:calc(var(--spacing) * 10);border-radius:var(--radius-sm);border-style:var(--tw-border-style);border-width:1px;border-color:var(--ink);background-color:var(--ink);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--paper);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;justify-content:center;align-items:center;text-decoration-line:none;transition-duration:.15s;display:inline-flex}@media (hover:hover){.btn-primary:hover{border-color:var(--accent);background-color:var(--accent);color:var(--paper)}}.btn-primary:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--accent);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-offset-color:var(--paper);--tw-outline-style:none;outline-style:none}.btn-quiet{min-height:calc(var(--spacing) * 9);align-items:center;gap:var(--spacing);border-radius:var(--radius-sm);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 2);padding-block:var(--spacing);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--muted);text-decoration-line:underline;-webkit-text-decoration-color:var(--rule);-webkit-text-decoration-color:var(--rule);text-decoration-color:var(--rule);text-underline-offset:4px;background-color:#0000;border-width:0;display:inline-flex}@media (hover:hover){.btn-quiet:hover{color:var(--ink);-webkit-text-decoration-color:var(--accent);-webkit-text-decoration-color:var(--accent);text-decoration-color:var(--accent)}}.btn:disabled,.btn-primary:disabled,.btn-danger:disabled{cursor:not-allowed;border-color:var(--rule);background-color:var(--paper-2);color:var(--muted)}@media (hover:hover){:is(.btn:disabled,.btn-primary:disabled,.btn-danger:disabled):hover{background-color:var(--paper-2);color:var(--muted)}}.btn-danger,button.danger{min-height:calc(var(--spacing) * 9);border-radius:var(--radius-sm);border-style:var(--tw-border-style);border-width:1px;border-color:var(--down);padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--down);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;background-color:#0000;justify-content:center;align-items:center;transition-duration:.15s;display:inline-flex}@media (hover:hover){:is(.btn-danger,button.danger):hover{background-color:var(--down);color:var(--paper)}}.badge{padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * .5);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);white-space:nowrap;color:var(--ink-2);background:var(--ink);border-radius:3.40282e38px;align-items:center;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.badge{background:color-mix(in oklab, var(--ink) 8%, transparent)}}.badge.selected,.badge.loved,.badge.ok{color:var(--loved);background:var(--loved)}@supports (color:color-mix(in lab, red, red)){.badge.selected,.badge.loved,.badge.ok{background:color-mix(in oklab, var(--loved) 14%, transparent)}}.badge.good,.badge.assessed,.badge.triaged,.badge.degraded{color:var(--good);background:var(--good)}@supports (color:color-mix(in lab, red, red)){.badge.good,.badge.assessed,.badge.triaged,.badge.degraded{background:color-mix(in oklab, var(--good) 14%, transparent)}}.badge.down,.badge.excluded,.badge.failed,.badge.dry_run{color:var(--down);background:var(--down)}@supports (color:color-mix(in lab, red, red)){.badge.down,.badge.excluded,.badge.failed,.badge.dry_run{background:color-mix(in oklab, var(--down) 14%, transparent)}}.badge.shortlisted,.badge.admitted,.badge.eligible,.badge.cleared,.badge.reason,.badge.running,.badge.requested{color:var(--muted);background:var(--muted)}@supports (color:color-mix(in lab, red, red)){.badge.shortlisted,.badge.admitted,.badge.eligible,.badge.cleared,.badge.reason,.badge.running,.badge.requested{background:color-mix(in oklab, var(--muted) 14%, transparent)}}.badge.restart,.badge.warn{color:var(--warn);background:var(--warn)}@supports (color:color-mix(in lab, red, red)){.badge.restart,.badge.warn{background:color-mix(in oklab, var(--warn) 16%, transparent)}}.kv{column-gap:calc(var(--spacing) * 5);row-gap:calc(var(--spacing) * 1.5);grid-template-columns:minmax(9rem,1fr) 2fr;display:grid}.kv dt{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--muted)}.kv dd{min-width:0;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));overflow-wrap:anywhere;margin:0}.scroll-x{overscroll-behavior-inline:contain;max-width:100%;overflow-x:auto}.scroll-x>table{min-width:max-content}.scroll-x.tall{overscroll-behavior-block:contain;max-height:70vh;overflow-y:auto}.prose-body{--tw-leading:1.72;color:var(--ink);font-size:1.0625rem;line-height:1.72}.prose-body>*+*{margin-top:calc(var(--spacing) * 5)}.prose-body h2{margin-top:calc(var(--spacing) * 10);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--rule);padding-top:calc(var(--spacing) * 4);font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height));--tw-leading:1.15;--tw-font-weight:var(--font-weight-semibold);line-height:1.15;font-weight:var(--font-weight-semibold);--tw-tracking:-.01em;letter-spacing:-.01em}.prose-body h3{margin-top:calc(var(--spacing) * 8);font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-leading:var(--leading-tight);line-height:var(--leading-tight);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.prose-body a{color:var(--ink);text-decoration-line:underline;-webkit-text-decoration-color:var(--rule);-webkit-text-decoration-color:var(--rule);text-decoration-color:var(--rule);text-underline-offset:4px}@media (hover:hover){.prose-body a:hover{color:var(--ink);-webkit-text-decoration-color:var(--accent);-webkit-text-decoration-color:var(--accent);text-decoration-color:var(--accent)}}.prose-body img{margin-inline:auto;margin-block:calc(var(--spacing) * 7);border-radius:var(--radius-sm);text-align:center;max-width:100%;height:auto;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--muted);font-style:italic;display:block}.prose-body pre{border-radius:var(--radius-sm);background-color:var(--paper-2);max-width:100%;padding:calc(var(--spacing) * 4);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);overflow-x:auto}.prose-body :not(pre)>code{border-radius:var(--radius-sm);background-color:var(--paper-2);padding-inline:var(--spacing);padding-block:calc(var(--spacing) * .5);font-size:.9em}.prose-body blockquote{margin-block:calc(var(--spacing) * 7);border-left-style:var(--tw-border-style);border-left-width:2px;border-color:var(--rule-strong);padding-left:calc(var(--spacing) * 5);color:var(--ink-2)}.prose-body ul{padding-left:calc(var(--spacing) * 6);list-style-type:disc}.prose-body ol{padding-left:calc(var(--spacing) * 6);list-style-type:decimal}.prose-body li::marker{color:var(--muted)}.prose-body li+li{margin-top:calc(var(--spacing) * 2)}.rating{margin-block:calc(var(--spacing) * 5);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));flex-wrap:wrap;align-items:center;display:flex}.rating-prompt{margin-right:calc(var(--spacing) * 3);--tw-tracking:.1em;letter-spacing:.1em;color:var(--muted);text-transform:uppercase;font-size:.72rem}.rating button[data-label]:not(.clear){min-height:calc(var(--spacing) * 10);border-color:var(--rule);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);color:var(--ink);background-color:#0000;border-radius:0;margin-left:-1px}.rating button[data-label]:not(.clear):first-child{border-top-left-radius:var(--radius-sm);border-bottom-left-radius:var(--radius-sm);margin-left:0}.rating button[data-label]:not(.clear):last-child{border-top-right-radius:var(--radius-sm);border-bottom-right-radius:var(--radius-sm)}@media (hover:hover){.rating button[data-label]:not(.clear):hover{z-index:10;border-color:var(--ink);background-color:var(--paper-2)}}.rating button[data-label]:not(.clear).active{z-index:10;border-color:var(--ink);background-color:var(--ink);color:var(--paper)}.rating button.clear{margin-left:calc(var(--spacing) * 2);min-height:calc(var(--spacing) * 10);border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 2);color:var(--muted);text-decoration-line:underline;-webkit-text-decoration-color:var(--rule);-webkit-text-decoration-color:var(--rule);text-decoration-color:var(--rule);text-underline-offset:4px;background-color:#0000;border-width:0}@media (hover:hover){.rating button.clear:hover{color:var(--ink)}}.rating button.clear.active{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--ink)}.rating-note{margin-top:calc(var(--spacing) * 2);width:100%;max-width:var(--container-md);align-items:center;gap:calc(var(--spacing) * 2);color:var(--muted);flex-basis:100%;display:flex}.rating-note input{flex:1}.flash{margin-block:calc(var(--spacing) * 4);border-left-style:var(--tw-border-style);border-left-width:4px;border-color:var(--accent);background-color:var(--paper-2);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.error{border-left-style:var(--tw-border-style);border-left-width:4px;border-color:var(--down);background-color:var(--paper-2);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--down)}.notice{border-left-style:var(--tw-border-style);border-left-width:4px;border-color:var(--warn);background-color:var(--paper-2);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--ink-2)}ul.error,ul.notice{padding-left:calc(var(--spacing) * 8);list-style-type:disc}.dashboard{margin-inline:auto;margin-block:calc(var(--spacing) * 8);max-width:var(--container-7xl);padding-inline:calc(var(--spacing) * 4);padding-bottom:calc(var(--spacing) * 6);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-leading:var(--leading-normal);line-height:var(--leading-normal)}@media (min-width:40rem){.dashboard{padding-inline:calc(var(--spacing) * 6)}}.dashboard>*+*{margin-top:calc(var(--spacing) * 4)}.dashboard h1{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height));--tw-leading:var(--leading-tight);line-height:var(--leading-tight);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:-.01em;letter-spacing:-.01em}.dashboard h2{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.dashboard h3{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.dashboard>h2{margin-top:calc(var(--spacing) * 9);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--rule);padding-top:calc(var(--spacing) * 3);--tw-font-weight:var(--font-weight-semibold);font-size:.78rem;font-weight:var(--font-weight-semibold);--tw-tracking:.14em;letter-spacing:.14em;color:var(--muted);text-transform:uppercase}.dashboard>p,.dashboard>ul,.dashboard>ol{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.dashboard code{border-radius:var(--radius-sm);background-color:var(--paper-2);padding-inline:var(--spacing);padding-block:calc(var(--spacing) * .5);font-size:.85em}.dashboard pre code{background-color:#0000;padding:0}.dashboard table a{color:var(--ink);text-decoration-line:underline;-webkit-text-decoration-color:var(--rule);-webkit-text-decoration-color:var(--rule);text-decoration-color:var(--rule);text-underline-offset:2px}@media (hover:hover){.dashboard table a:hover{color:var(--ink);-webkit-text-decoration-color:var(--accent);-webkit-text-decoration-color:var(--accent);text-decoration-color:var(--accent)}}.page-head{justify-content:space-between;align-items:flex-end;column-gap:calc(var(--spacing) * 6);row-gap:calc(var(--spacing) * 3);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--rule);padding-bottom:calc(var(--spacing) * 4);flex-wrap:wrap;display:flex}.page-head>div:first-child{min-width:0}.dashboard>.page-head+*{margin-top:calc(var(--spacing) * 6)}.page-desc{margin-top:calc(var(--spacing) * 1.5);max-width:74ch;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--muted)}.page-desc a{color:var(--accent)}.page-actions{align-items:center;gap:calc(var(--spacing) * 2);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));flex-wrap:wrap;display:flex}.page-eyebrow{--tw-tracking:.14em;letter-spacing:.14em;color:var(--muted);text-transform:uppercase;font-size:.7rem}.tiles{gap:calc(var(--spacing) * 3);grid-template-columns:repeat(auto-fit,minmax(min(10.5rem,100%),1fr));display:grid}.tile{border-style:var(--tw-border-style);border-width:1px;border-color:var(--rule);min-width:0;padding-inline:calc(var(--spacing) * 3.5);padding-block:calc(var(--spacing) * 3)}.tile-label{--tw-font-weight:var(--font-weight-medium);font-size:.68rem;font-weight:var(--font-weight-medium);--tw-tracking:.12em;letter-spacing:.12em;color:var(--muted);text-transform:uppercase;display:block}.tile-num{margin-top:var(--spacing);--tw-leading:var(--leading-tight);font-size:1.55rem;line-height:var(--leading-tight);--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:-.01em;letter-spacing:-.01em;font-variant-numeric:tabular-nums;overflow-wrap:anywhere;display:block}.tile-delta{margin-top:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--muted);overflow-wrap:anywhere;display:block}.cards{gap:calc(var(--spacing) * 4);grid-template-columns:repeat(auto-fit,minmax(min(20rem,100%),1fr));display:grid}.card{border-style:var(--tw-border-style);border-width:1px;border-color:var(--rule);min-width:0;padding:calc(var(--spacing) * 4)}.card>*+*{margin-top:calc(var(--spacing) * 3)}.card h2,.card h3{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.card>h2:first-child,.card>h3:first-child{margin-top:0}.card p,.card li{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.muted,.meta{color:var(--muted)}.jobs a,.picks>li>a:first-child{color:var(--ink);text-decoration-line:none}@media (hover:hover){:is(.jobs a,.picks>li>a:first-child):hover{color:var(--accent)}}.num{text-align:right;white-space:nowrap;--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.cell-wrap{white-space:normal;overflow-wrap:anywhere;min-width:10rem;max-width:24rem}.cell-tight{white-space:nowrap}.filters{align-items:flex-end;column-gap:calc(var(--spacing) * 4);row-gap:calc(var(--spacing) * 3);border-block-style:var(--tw-border-style);border-block-width:1px;border-color:var(--rule);padding-block:calc(var(--spacing) * 3);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));flex-wrap:wrap;display:flex}.filters label{gap:var(--spacing);--tw-font-weight:var(--font-weight-medium);font-size:.66rem;font-weight:var(--font-weight-medium);--tw-tracking:.1em;letter-spacing:.1em;color:var(--muted);text-transform:uppercase;display:grid}.filters input,.filters select,.filters textarea{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal);color:var(--ink);text-transform:none}.filters select,.filters input[type=date]{min-width:calc(var(--spacing) * 32)}.filters .filter-actions{align-items:center;gap:calc(var(--spacing) * 3);display:flex}.form-inline{align-items:center;gap:calc(var(--spacing) * 3);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));flex-wrap:wrap;display:flex}.form-inline label{align-items:center;gap:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));display:flex}.table-filter{margin-block:calc(var(--spacing) * 2);width:calc(var(--spacing) * 80);max-width:100%;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.pager{justify-content:space-between;align-items:center;column-gap:calc(var(--spacing) * 6);row-gap:calc(var(--spacing) * 2);padding-block:var(--spacing);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--muted);flex-wrap:wrap;display:flex}.pager a{color:var(--ink);text-decoration-line:none}@media (hover:hover){.pager a:hover{color:var(--accent)}}.run-nav{justify-content:space-between;align-items:center;column-gap:calc(var(--spacing) * 6);row-gap:calc(var(--spacing) * 2);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));flex-wrap:wrap;display:flex}.run-nav a{color:var(--ink);text-decoration-line:none}@media (hover:hover){.run-nav a:hover{color:var(--accent)}}.tabs{align-items:center;column-gap:calc(var(--spacing) * 6);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--rule);--tw-font-weight:var(--font-weight-medium);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:.72rem;font-weight:var(--font-weight-medium);--tw-tracking:.12em;letter-spacing:.12em;text-transform:uppercase;flex-wrap:wrap;display:flex}.tabs a{min-height:calc(var(--spacing) * 10);border-bottom-style:var(--tw-border-style);color:var(--muted);border-color:#0000;border-bottom-width:2px;align-items:center;margin-bottom:-1px;text-decoration-line:none;display:flex}@media (hover:hover){.tabs a:hover{color:var(--ink)}}.tabs a.active{border-color:var(--accent);color:var(--ink)}.pagination{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--muted)}tr.superseded>td,tr.superseded a{color:var(--muted)}td .rating,.picks .rating{margin-block:calc(var(--spacing) * 1.5);row-gap:var(--spacing);max-width:15rem;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}td .rating-prompt,.picks .rating-prompt{display:none}td .rating button[data-label]:not(.clear),.picks .rating button[data-label]:not(.clear){min-height:calc(var(--spacing) * 8);padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}td .rating button.clear,.picks .rating button.clear{margin-left:calc(var(--spacing) * 2);min-height:calc(var(--spacing) * 8);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));padding-inline:0}td .rating-note{margin-top:var(--spacing);gap:calc(var(--spacing) * 1.5);max-width:15rem;font-size:.7rem}td .rating-note input{padding-inline:calc(var(--spacing) * 1.5);padding-block:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.picks .rating{max-width:none}pre.block,pre.preview,pre.journal{border-radius:var(--radius-sm);background-color:var(--paper-2);padding:calc(var(--spacing) * 3);--tw-leading:var(--leading-relaxed);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.8125rem;line-height:var(--leading-relaxed);white-space:pre-wrap;overflow-wrap:anywhere;overflow:auto}pre.block{max-height:calc(var(--spacing) * 64)}pre.preview{max-height:calc(var(--spacing) * 56)}pre.journal{max-height:34rem;padding:calc(var(--spacing) * 4)}pre.prompt,pre.learned{max-height:32rem}td>pre.preview{max-height:calc(var(--spacing) * 24);max-width:var(--container-md);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--muted);background-color:#0000;padding:0}details.disclosure{border-style:var(--tw-border-style);border-width:1px;border-color:var(--rule)}details.disclosure>summary{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}details.disclosure[open]>summary{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--rule)}details.disclosure>.disclosure-body{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}details.disclosure>.disclosure-body>*+*{margin-top:calc(var(--spacing) * 3)}details.signals>summary{min-width:0}.signals-body{margin-top:calc(var(--spacing) * 2);max-width:var(--container-2xl);border-left-style:var(--tw-border-style);border-left-width:2px;border-color:var(--rule);padding-left:calc(var(--spacing) * 3);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.signals-body>*+*{margin-top:calc(var(--spacing) * 2)}.signals-body ul{padding-left:calc(var(--spacing) * 5);list-style-type:disc}.funnel{border-radius:var(--radius-sm);background-color:var(--paper-2);width:100%;min-width:6rem}.funnel>*{border-radius:var(--radius-sm);background-color:var(--good);min-width:1px;display:block}.funnel-cell{vertical-align:middle;width:14rem;min-width:8rem}.sparklines{gap:calc(var(--spacing) * 4);grid-template-columns:repeat(auto-fit,minmax(min(18rem,100%),1fr));display:grid}.spark-figure{border-style:var(--tw-border-style);border-width:1px;border-color:var(--rule);min-width:0;padding:calc(var(--spacing) * 3);margin:0}.spark-figure figcaption{margin-bottom:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-semibold);font-size:.7rem;font-weight:var(--font-weight-semibold);--tw-tracking:.1em;letter-spacing:.1em;color:var(--ink);text-transform:uppercase}.spark-figure figcaption .muted{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal);--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal);text-transform:none}.spark{width:100%;max-width:100%;height:auto}.spark .s0{fill:var(--good)}.spark .s1{fill:var(--loved)}.spark .s2{fill:var(--down)}.spark .s3{fill:var(--accent)}.spark .s4{fill:var(--muted)}.spark .s5{fill:var(--ink)}.spark .line{stroke:var(--accent)}.spark-axis{margin-top:var(--spacing);--tw-tracking:.08em;letter-spacing:.08em;text-transform:uppercase;justify-content:space-between;font-size:.66rem;display:flex}.spark-legend{margin-top:calc(var(--spacing) * 2);column-gap:calc(var(--spacing) * 3);row-gap:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--muted);flex-wrap:wrap;display:flex}.spark-legend li{align-items:center;gap:calc(var(--spacing) * 1.5);display:flex}.settings-form>*+*{margin-top:calc(var(--spacing) * 5)}.setting{gap:calc(var(--spacing) * 2);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--rule);padding-block:calc(var(--spacing) * 3);display:grid}@media (min-width:48rem){.setting{gap:calc(var(--spacing) * 6);grid-template-columns:minmax(0,19rem) minmax(0,1fr)}}.setting:first-of-type{border-top-style:var(--tw-border-style);padding-top:var(--spacing);border-top-width:0}.setting-label{min-width:0}.setting-label label{display:inline-block}.setting-label code{--tw-font-weight:var(--font-weight-medium);font-size:.85rem;font-weight:var(--font-weight-medium);color:var(--ink);overflow-wrap:anywhere;background-color:#0000;padding:0}.setting-input{min-width:0}.setting-input>input,.setting-input>select,.setting-input>textarea{width:100%;max-width:var(--container-lg)}.help,.default{margin-top:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--muted);overflow-wrap:anywhere}.setting-label .help{max-width:42ch}.secret{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--ink-2)}code.lines{margin-top:var(--spacing);white-space:pre-wrap;display:block}button.reset{margin-left:var(--spacing);border-style:var(--tw-border-style);min-height:0;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--muted);text-decoration-line:underline;-webkit-text-decoration-color:var(--rule);-webkit-text-decoration-color:var(--rule);text-decoration-color:var(--rule);text-underline-offset:2px;background-color:#0000;border-width:0;border-radius:0;padding:0;display:inline}@media (hover:hover){button.reset:hover{color:var(--ink)}}.save-bar{z-index:20;margin-inline:calc(var(--spacing) * -4);margin-top:calc(var(--spacing) * 6);justify-content:flex-end;align-items:center;column-gap:calc(var(--spacing) * 4);row-gap:calc(var(--spacing) * 2);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--rule);background-color:var(--paper);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);flex-wrap:wrap;display:flex;position:sticky;bottom:0}@media (min-width:40rem){.save-bar{margin-inline:calc(var(--spacing) * -6);padding-inline:calc(var(--spacing) * 6)}}.save-bar{box-shadow:0 -10px 18px -14px var(--ink)}@supports (color:color-mix(in lab, red, red)){.save-bar{box-shadow:0 -10px 18px -14px color-mix(in oklab, var(--ink) 55%, transparent)}}.save-bar .meta{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));margin-right:auto}@media (max-width:40rem){.kv{display:block}.kv dt{margin-top:calc(var(--spacing) * 2)}.rating{align-items:stretch}.rating-prompt,.rating-note{flex-basis:100%}.rating-prompt{margin-bottom:calc(var(--spacing) * 2)}.filters label,.filters input,.filters select{width:100%}}}@layer utilities{.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.top-1{top:var(--spacing)}.top-3{top:calc(var(--spacing) * 3)}.top-4{top:calc(var(--spacing) * 4)}.top-20{top:calc(var(--spacing) * 20)}.top-40{top:calc(var(--spacing) * 40)}.top-120{top:calc(var(--spacing) * 120)}.left-3{left:calc(var(--spacing) * 3)}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:0}.mx-auto{margin-inline:auto}.my-4{margin-block:calc(var(--spacing) * 4)}.my-8{margin-block:calc(var(--spacing) * 8)}.my-12{margin-block:calc(var(--spacing) * 12)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-7{margin-top:calc(var(--spacing) * 7)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-10{margin-top:calc(var(--spacing) * 10)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mt-16{margin-top:calc(var(--spacing) * 16)}.mt-auto{margin-top:auto}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.mb-9{margin-bottom:calc(var(--spacing) * 9)}.mb-10{margin-bottom:calc(var(--spacing) * 10)}.ml-1{margin-left:var(--spacing)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-4{height:calc(var(--spacing) * 4)}.max-h-\[calc\(100vh-2rem\)\]{max-height:calc(100vh - 2rem)}.min-h-8{min-height:calc(var(--spacing) * 8)}.min-h-10{min-height:calc(var(--spacing) * 10)}.min-h-11{min-height:calc(var(--spacing) * 11)}.min-h-24{min-height:calc(var(--spacing) * 24)}.min-h-\[68vh\]{min-height:68vh}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-4{width:calc(var(--spacing) * 4)}.w-64{width:calc(var(--spacing) * 64)}.w-full{width:100%}.max-w-7xl{max-width:var(--container-7xl)}.max-w-\[58ch\]{max-width:58ch}.max-w-\[68ch\]{max-width:68ch}.max-w-md{max-width:var(--container-md)}.min-w-0{min-width:0}.min-w-\[13rem\]{min-width:13rem}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.-translate-y-20{--tw-translate-y:calc(var(--spacing) * -20);translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.resize{resize:both}.scroll-mt-4{scroll-margin-top:calc(var(--spacing) * 4)}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-\[1fr_auto\]{grid-template-columns:1fr auto}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-2{column-gap:calc(var(--spacing) * 2)}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-y-1{row-gap:var(--spacing)}.gap-y-1\.5{row-gap:calc(var(--spacing) * 1.5)}.gap-y-3{row-gap:calc(var(--spacing) * 3)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-rule>:not(:last-child)){border-color:var(--rule)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded-sm{border-radius:var(--radius-sm)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-y-0{border-block-style:var(--tw-border-style);border-block-width:0}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-b-\[3px\]{border-bottom-style:var(--tw-border-style);border-bottom-width:3px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-double{--tw-border-style:double;border-style:double}.border-accent{border-color:var(--accent)}.border-ink{border-color:var(--ink)}.border-rule{border-color:var(--rule)}.border-rule-strong{border-color:var(--rule-strong)}.border-transparent{border-color:#0000}.bg-accent{background-color:var(--accent)}.bg-ink{background-color:var(--ink)}.bg-paper{background-color:var(--paper)}.bg-paper-2{background-color:var(--paper-2)}.bg-transparent{background-color:#0000}.fill-none{fill:none}.stroke-current{stroke:currentColor}.p-0{padding:0}.p-4{padding:calc(var(--spacing) * 4)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-0{padding-block:0}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-16{padding-block:calc(var(--spacing) * 16)}.py-20{padding-block:calc(var(--spacing) * 20)}.pt-0{padding-top:0}.pt-1{padding-top:var(--spacing)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pt-7{padding-top:calc(var(--spacing) * 7)}.pt-8{padding-top:calc(var(--spacing) * 8)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-9{padding-bottom:calc(var(--spacing) * 9)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-5{padding-left:calc(var(--spacing) * 5)}.pl-6{padding-left:calc(var(--spacing) * 6)}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.font-sans{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}.font-serif{font-family:Newsreader,Iowan Old Style,Palatino Linotype,Georgia,serif}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.68rem\]{font-size:.68rem}.text-\[0\.72rem\]{font-size:.72rem}.text-\[0\.95rem\]{font-size:.95rem}.leading-\[1\.1\]{--tw-leading:1.1;line-height:1.1}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[-0\.01em\]{--tw-tracking:-.01em;letter-spacing:-.01em}.tracking-\[-0\.02em\]{--tw-tracking:-.02em;letter-spacing:-.02em}.tracking-\[0\.1em\]{--tw-tracking:.1em;letter-spacing:.1em}.tracking-\[0\.08em\]{--tw-tracking:.08em;letter-spacing:.08em}.tracking-\[0\.12em\]{--tw-tracking:.12em;letter-spacing:.12em}.tracking-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.whitespace-nowrap{white-space:nowrap}.text-accent{color:var(--accent)}.text-down{color:var(--down)}.text-good{color:var(--good)}.text-ink{color:var(--ink)}.text-ink-2{color:var(--ink-2)}.text-loved{color:var(--loved)}.text-muted{color:var(--muted)}.text-paper{color:var(--paper)}.text-warn{color:var(--warn)}.lowercase{text-transform:lowercase}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.decoration-rule{-webkit-text-decoration-color:var(--rule);-webkit-text-decoration-color:var(--rule);text-decoration-color:var(--rule)}.underline-offset-4{text-underline-offset:4px}.shadow,.shadow\/backfill,.shadow\/compatibility,.shadow\/dry,.shadow\/maintenance{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-accent{--tw-ring-color:var(--accent)}.ring-offset-2{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.ring-offset-paper{--tw-ring-offset-color:var(--paper)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.grayscale{--tw-grayscale:grayscale(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.group-hover\:text-accent:is(:where(.group):hover *){color:var(--accent)}}.marker\:text-muted ::marker{color:var(--muted)}.marker\:text-muted::marker{color:var(--muted)}.marker\:text-muted ::-webkit-details-marker{color:var(--muted)}.marker\:text-muted::-webkit-details-marker{color:var(--muted)}@media (hover:hover){.hover\:border-accent:hover{border-color:var(--accent)}.hover\:bg-paper-2:hover{background-color:var(--paper-2)}.hover\:text-accent:hover{color:var(--accent)}.hover\:text-accent-hover:hover{color:var(--accent-hover)}.hover\:text-ink:hover{color:var(--ink)}.hover\:decoration-accent:hover{-webkit-text-decoration-color:var(--accent);-webkit-text-decoration-color:var(--accent);text-decoration-color:var(--accent)}}.focus\:translate-y-0:focus{--tw-translate-y:0px;translate:var(--tw-translate-x) var(--tw-translate-y)}.focus\:border-ink:focus{border-color:var(--ink)}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}@media (min-width:40rem){.sm\:col-start-2{grid-column-start:2}.sm\:mt-12{margin-top:calc(var(--spacing) * 12)}.sm\:mt-14{margin-top:calc(var(--spacing) * 14)}.sm\:mt-16{margin-top:calc(var(--spacing) * 16)}.sm\:inline{display:inline}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}.sm\:py-4{padding-block:calc(var(--spacing) * 4)}.sm\:pt-6{padding-top:calc(var(--spacing) * 6)}.sm\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.sm\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}@media (min-width:48rem){.md\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}}@media (min-width:64rem){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:80rem){.xl\:w-72{width:calc(var(--spacing) * 72)}}.\[\&_\.comment-meta\]\:font-sans .comment-meta{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}.\[\&_\.comment-meta\]\:text-sm .comment-meta{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.\[\&_\.comment-meta\]\:text-muted .comment-meta{color:var(--muted)}.\[\&_blockquote\.reply\]\:ml-6 blockquote.reply{margin-left:calc(var(--spacing) * 6)}.\[\&\>p\:first-child\]\:first-letter\:float-left>p:first-child:first-letter{float:left}.\[\&\>p\:first-child\]\:first-letter\:mt-1>p:first-child:first-letter{margin-top:var(--spacing)}.\[\&\>p\:first-child\]\:first-letter\:mr-2>p:first-child:first-letter{margin-right:calc(var(--spacing) * 2)}.\[\&\>p\:first-child\]\:first-letter\:font-serif>p:first-child:first-letter{font-family:Newsreader,Iowan Old Style,Palatino Linotype,Georgia,serif}.\[\&\>p\:first-child\]\:first-letter\:text-\[4\.6rem\]>p:first-child:first-letter{font-size:4.6rem}.\[\&\>p\:first-child\]\:first-letter\:leading-\[0\.72\]>p:first-child:first-letter{--tw-leading:.72;line-height:.72}.\[\&\>p\:first-child\]\:first-letter\:font-semibold>p:first-child:first-letter{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}}@font-face{font-family:Newsreader;font-style:normal;font-weight:200 800;font-stretch:100%;font-display:swap;src:url(/static/Newsreader.woff2)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Newsreader;font-style:italic;font-weight:200 800;font-stretch:100%;font-display:swap;src:url(/static/Newsreader-italic.woff2)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--paper:#f6f3ec;--paper-2:#ece7db;--ink:#1c1b18;--ink-2:#4f4b43;--muted:#7a7568;--rule:#d8d2c4;--rule-strong:#1c1b18;--accent:#a3231f;--accent-hover:#7c1a16;--loved:#2f6f46;--good:#2f6a8f;--down:#9c3f36;--warn:#8a6d1f;color-scheme:light}@media (prefers-color-scheme:dark){:root:not([data-theme=light]){--paper:#151513;--paper-2:#1e1d1a;--ink:#ebe6da;--ink-2:#bfb9aa;--muted:#8f8a7d;--rule:#3a3833;--rule-strong:#ebe6da;--accent:#ef8c82;--accent-hover:#f7aaa2;--loved:#7fcb92;--good:#7fb8d8;--down:#ea8f84;--warn:#d9b45a;color-scheme:dark}}:root[data-theme=dark]{--paper:#151513;--paper-2:#1e1d1a;--ink:#ebe6da;--ink-2:#bfb9aa;--muted:#8f8a7d;--rule:#3a3833;--rule-strong:#ebe6da;--accent:#ef8c82;--accent-hover:#f7aaa2;--loved:#7fcb92;--good:#7fb8d8;--down:#ea8f84;--warn:#d9b45a;color-scheme:dark}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false} \ No newline at end of file diff --git a/src/web/tailwind.css b/src/web/tailwind.css index 9e86238..70a8282 100644 --- a/src/web/tailwind.css +++ b/src/web/tailwind.css @@ -64,29 +64,47 @@ h1, h2, h3 { @apply text-ink; font-optical-sizing:auto; } button, input, select, textarea { font:inherit; @apply rounded-sm border border-rule bg-paper px-2.5 py-1.5 text-ink transition-colors duration-150 placeholder:text-muted focus:border-ink focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-paper; } button { @apply cursor-pointer; } + button:disabled, input:disabled, select:disabled, textarea:disabled { @apply cursor-not-allowed border-rule bg-paper-2 text-muted; } textarea { @apply min-h-28; } + summary { @apply cursor-pointer; } table { @apply w-full border-collapse font-sans text-sm leading-snug; font-variant-numeric:tabular-nums; } thead { @apply sticky top-0 z-10 bg-paper; } th, td { @apply border-b border-rule px-2 py-2 text-left align-top; } th { @apply text-[0.72rem] font-semibold uppercase tracking-[0.1em] text-muted; } tbody tr { @apply transition-colors duration-150 hover:bg-paper-2; } code, pre { @apply font-mono; } + meter { @apply block h-2 w-full rounded-sm; -webkit-appearance:none; appearance:none; background:var(--rule); } + meter::-webkit-meter-bar { background:var(--rule); border:0; border-radius:2px; height:0.5rem; } + meter::-webkit-meter-optimum-value, meter::-webkit-meter-suboptimum-value, meter::-webkit-meter-even-less-good-value { background:var(--good); border-radius:2px; } + meter.over::-webkit-meter-optimum-value, meter.over::-webkit-meter-suboptimum-value, meter.over::-webkit-meter-even-less-good-value { background:var(--down); } + meter::-moz-meter-bar { background:var(--good); border-radius:2px; } + meter.over::-moz-meter-bar { background:var(--down); } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior:auto !important; transition-duration:0.01ms !important; } } } + @layer components { + /* ---------- shared controls ---------- */ .btn, .button { @apply inline-flex min-h-10 items-center justify-center rounded-sm border border-rule-strong bg-transparent px-3 py-1.5 font-sans text-sm font-medium text-ink no-underline transition-colors duration-150 hover:bg-ink hover:text-paper focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-paper; } .btn-primary { @apply inline-flex min-h-10 items-center justify-center rounded-sm border border-ink bg-ink px-3 py-1.5 font-sans text-sm font-medium text-paper no-underline transition-colors duration-150 hover:border-accent hover:bg-accent hover:text-paper focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-paper; } - .badge { @apply inline-flex items-center rounded-full px-2 py-0.5 font-sans text-xs font-medium; } + .btn-quiet { @apply inline-flex min-h-9 items-center gap-1 rounded-sm border-0 bg-transparent px-2 py-1 font-sans text-sm text-muted underline decoration-rule underline-offset-4 hover:text-ink hover:decoration-accent; } + .btn:disabled, .btn-primary:disabled, .btn-danger:disabled { @apply cursor-not-allowed border-rule bg-paper-2 text-muted hover:bg-paper-2 hover:text-muted; } + .btn-danger, button.danger { @apply inline-flex min-h-9 items-center justify-center rounded-sm border border-down bg-transparent px-2.5 py-1 font-sans text-sm font-medium text-down transition-colors duration-150 hover:bg-down hover:text-paper; } + + .badge { @apply inline-flex items-center whitespace-nowrap rounded-full px-2 py-0.5 font-sans text-xs font-medium leading-5; color:var(--ink-2); background:color-mix(in oklab, var(--ink) 8%, transparent); } .badge.selected, .badge.loved, .badge.ok { color:var(--loved); background:color-mix(in oklab, var(--loved) 14%, transparent); } .badge.good, .badge.assessed, .badge.triaged, .badge.degraded { color:var(--good); background:color-mix(in oklab, var(--good) 14%, transparent); } .badge.down, .badge.excluded, .badge.failed, .badge.dry_run { color:var(--down); background:color-mix(in oklab, var(--down) 14%, transparent); } .badge.shortlisted, .badge.admitted, .badge.eligible, .badge.cleared, .badge.reason, .badge.running, .badge.requested { color:var(--muted); background:color-mix(in oklab, var(--muted) 14%, transparent); } - .kv { @apply grid grid-cols-[minmax(9rem,1fr)_2fr] gap-x-5 gap-y-1; } - .kv dt { @apply text-muted; } - .kv dd { @apply m-0 min-w-0; overflow-wrap:anywhere; } + .badge.restart, .badge.warn { color:var(--warn); background:color-mix(in oklab, var(--warn) 16%, transparent); } + + .kv { @apply grid grid-cols-[minmax(9rem,1fr)_2fr] gap-x-5 gap-y-1.5; } + .kv dt { @apply text-sm text-muted; } + .kv dd { @apply m-0 min-w-0 text-sm; overflow-wrap:anywhere; } .scroll-x { @apply max-w-full overflow-x-auto; overscroll-behavior-inline:contain; } .scroll-x > table { min-width:max-content; } + .scroll-x.tall { @apply max-h-[70vh] overflow-y-auto; overscroll-behavior-block:contain; } + .prose-body { @apply text-[1.0625rem] leading-[1.72] text-ink; } .prose-body > * + * { @apply mt-5; } .prose-body h2 { @apply mt-10 border-t border-rule pt-4 text-2xl font-semibold leading-[1.15] tracking-[-0.01em]; } @@ -100,32 +118,143 @@ .prose-body ol { @apply list-decimal pl-6; } .prose-body li::marker { color:var(--muted); } .prose-body li + li { @apply mt-2; } + .rating { @apply my-5 flex flex-wrap items-center font-sans text-sm; } .rating-prompt { @apply mr-3 text-[0.72rem] uppercase tracking-[0.1em] text-muted; } .rating button[data-label]:not(.clear) { @apply -ml-px min-h-10 rounded-none border-rule bg-transparent px-3 py-1.5 text-ink first:ml-0 first:rounded-l-sm last:rounded-r-sm hover:z-10 hover:border-ink hover:bg-paper-2; } .rating button[data-label]:not(.clear).active { @apply z-10 border-ink bg-ink text-paper; } .rating button.clear { @apply ml-2 min-h-10 border-0 bg-transparent px-2 text-muted underline decoration-rule underline-offset-4 hover:text-ink; } .rating button.clear.active { @apply font-semibold text-ink; } - .rating-note { @apply mt-2 flex basis-full items-center gap-2 text-muted; } + .rating-note { @apply mt-2 flex w-full max-w-md basis-full items-center gap-2 text-muted; } .rating-note input { @apply flex-1; } + .flash { @apply my-4 border-l-4 border-accent bg-paper-2 px-4 py-3 font-sans text-sm; } .error { @apply border-l-4 border-down bg-paper-2 px-4 py-3 font-sans text-sm text-down; } .notice { @apply border-l-4 border-warn bg-paper-2 px-4 py-3 font-sans text-sm text-ink-2; } - .dashboard { @apply mx-auto my-8 max-w-7xl px-4 font-sans text-base leading-normal sm:px-6; } - .dashboard h1 { @apply text-2xl font-semibold; } .dashboard h2 { @apply mt-7 text-lg font-semibold; } .dashboard h3 { @apply font-semibold; } - .cards { @apply my-4 grid gap-4; grid-template-columns:repeat(auto-fit,minmax(min(20rem,100%),1fr)); } + ul.error, ul.notice { @apply list-disc pl-8; } + + /* ---------- dashboard shell ---------- */ + .dashboard { @apply mx-auto my-8 max-w-7xl px-4 pb-6 font-sans text-base leading-normal sm:px-6; } + .dashboard > * + * { @apply mt-4; } + .dashboard h1 { @apply text-2xl font-semibold leading-tight tracking-[-0.01em]; } + .dashboard h2 { @apply text-lg font-semibold; } + .dashboard h3 { @apply text-sm font-semibold; } + .dashboard > h2 { @apply mt-9 border-t border-rule pt-3 text-[0.78rem] font-semibold uppercase tracking-[0.14em] text-muted; } + .dashboard > p, .dashboard > ul, .dashboard > ol { @apply text-sm; } + .dashboard code { @apply rounded-sm bg-paper-2 px-1 py-0.5 text-[0.85em]; } + .dashboard pre code { @apply bg-transparent p-0; } + .dashboard table a { @apply text-ink underline decoration-rule underline-offset-2 hover:text-ink hover:decoration-accent; } + + .page-head { @apply flex flex-wrap items-end justify-between gap-x-6 gap-y-3 border-b border-rule pb-4; } + .page-head > div:first-child { @apply min-w-0; } + .dashboard > .page-head + * { @apply mt-6; } + .page-desc { @apply mt-1.5 max-w-[74ch] text-sm text-muted; } + .page-desc a { @apply text-accent; } + .page-actions { @apply flex flex-wrap items-center gap-2 font-sans text-sm; } + .page-eyebrow { @apply text-[0.7rem] uppercase tracking-[0.14em] text-muted; } + + .tiles { @apply grid gap-3; grid-template-columns:repeat(auto-fit,minmax(min(10.5rem,100%),1fr)); } + .tile { @apply min-w-0 border border-rule px-3.5 py-3; } + .tile-label { @apply block text-[0.68rem] font-medium uppercase tracking-[0.12em] text-muted; } + .tile-num { @apply mt-1 block text-[1.55rem] font-semibold leading-tight tracking-[-0.01em]; font-variant-numeric:tabular-nums; overflow-wrap:anywhere; } + .tile-delta { @apply mt-1 block text-xs text-muted; overflow-wrap:anywhere; } + + .cards { @apply grid gap-4; grid-template-columns:repeat(auto-fit,minmax(min(20rem,100%),1fr)); } .card { @apply min-w-0 border border-rule p-4; } - .muted, .meta { @apply text-muted; } .num { @apply whitespace-nowrap text-right tabular-nums; } - .filters, form.filters, form.inline { @apply my-4 flex flex-wrap items-end gap-3 font-sans text-sm; } - .filters label, form.filters label { @apply grid gap-1; } + .card > * + * { @apply mt-3; } + .card h2 { @apply text-base font-semibold; } + .card h3 { @apply text-base font-semibold; } + .card > h2:first-child, .card > h3:first-child { @apply mt-0; } + .card p, .card li { @apply text-sm; } + + .muted, .meta { @apply text-muted; } + .jobs a, .picks > li > a:first-child { @apply text-ink no-underline hover:text-accent; } + .num { @apply whitespace-nowrap text-right tabular-nums; } + .cell-wrap { @apply min-w-[10rem] max-w-[24rem] whitespace-normal; overflow-wrap:anywhere; } + .cell-tight { @apply whitespace-nowrap; } + + .filters { @apply flex flex-wrap items-end gap-x-4 gap-y-3 border-y border-rule py-3 font-sans text-sm; } + .filters label { @apply grid gap-1 text-[0.66rem] font-medium uppercase tracking-[0.1em] text-muted; } + .filters input, .filters select, .filters textarea { @apply text-sm normal-case tracking-normal text-ink; } + .filters select, .filters input[type="date"] { @apply min-w-32; } + .filters .filter-actions { @apply flex items-center gap-3; } + .form-inline { @apply flex flex-wrap items-center gap-3 font-sans text-sm; } + .form-inline label { @apply flex items-center gap-2 text-sm; } .table-filter { @apply my-2 w-80 max-w-full font-sans text-sm; } - .pager, .run-nav, .tabs { @apply my-4 flex flex-wrap items-center gap-4 font-sans text-sm; } - pre.block, pre.preview, pre.journal { @apply overflow-auto whitespace-pre-wrap rounded-sm bg-paper-2 p-3 font-mono text-sm; overflow-wrap:anywhere; } - .funnel > * { @apply my-0.5 min-w-px bg-good; } - .spark { @apply h-auto max-w-full; } + + .pager { @apply flex flex-wrap items-center justify-between gap-x-6 gap-y-2 py-1 font-sans text-sm text-muted; } + .pager a { @apply text-ink no-underline hover:text-accent; } + .run-nav { @apply flex flex-wrap items-center justify-between gap-x-6 gap-y-2 font-sans text-sm; } + .run-nav a { @apply text-ink no-underline hover:text-accent; } + .tabs { @apply flex flex-wrap items-center gap-x-6 border-b border-rule font-sans text-[0.72rem] font-medium uppercase tracking-[0.12em]; } + .tabs a { @apply -mb-px flex min-h-10 items-center border-b-2 border-transparent text-muted no-underline hover:text-ink; } + .tabs a.active { @apply border-accent text-ink; } + .pagination { @apply font-sans text-sm text-muted; } + + tr.superseded > td { @apply text-muted; } + tr.superseded a { @apply text-muted; } + + /* the rating widget has to fit inside a table cell */ + td .rating, .picks .rating { @apply my-1.5 max-w-[15rem] gap-y-1 text-xs; } + td .rating-prompt, .picks .rating-prompt { @apply hidden; } + td .rating button[data-label]:not(.clear), .picks .rating button[data-label]:not(.clear) { @apply min-h-8 px-2.5 py-1 text-xs; } + td .rating button.clear, .picks .rating button.clear { @apply ml-2 min-h-8 px-0 text-xs; } + td .rating-note { @apply mt-1 max-w-[15rem] gap-1.5 text-[0.7rem]; } + td .rating-note input { @apply px-1.5 py-0.5 text-xs; } + .picks .rating { @apply max-w-none; } + + pre.block, pre.preview, pre.journal { @apply overflow-auto whitespace-pre-wrap rounded-sm bg-paper-2 p-3 font-mono text-[0.8125rem] leading-relaxed; overflow-wrap:anywhere; } + pre.block { @apply max-h-64; } + pre.preview { @apply max-h-56; } + pre.journal { @apply max-h-[34rem] p-4; } + pre.prompt, pre.learned { @apply max-h-[32rem]; } + td > pre.preview { @apply max-h-24 max-w-md bg-transparent p-0 text-xs text-muted; } + + details.disclosure { @apply border border-rule; } + details.disclosure > summary { @apply px-4 py-3 font-sans text-sm font-medium; } + details.disclosure[open] > summary { @apply border-b border-rule; } + details.disclosure > .disclosure-body { @apply px-4 py-3 text-sm; } + details.disclosure > .disclosure-body > * + * { @apply mt-3; } + details.signals > summary { @apply min-w-0; } + .signals-body { @apply mt-2 max-w-2xl border-l-2 border-rule pl-3 text-xs; } + .signals-body > * + * { @apply mt-2; } + .signals-body ul { @apply list-disc pl-5; } + + .funnel { @apply w-full min-w-[6rem] rounded-sm bg-paper-2; } + .funnel > * { @apply block min-w-px rounded-sm bg-good; } + .funnel-cell { @apply w-[14rem] min-w-[8rem] align-middle; } + + .sparklines { @apply grid gap-4; grid-template-columns:repeat(auto-fit,minmax(min(18rem,100%),1fr)); } + .spark-figure { @apply m-0 min-w-0 border border-rule p-3; } + .spark-figure figcaption { @apply mb-2 text-[0.7rem] font-semibold uppercase tracking-[0.1em] text-ink; } + .spark-figure figcaption .muted { @apply font-normal normal-case tracking-normal; } + .spark { @apply h-auto w-full max-w-full; } .spark .s0 { fill:var(--good); } .spark .s1 { fill:var(--loved); } .spark .s2 { fill:var(--down); } .spark .s3 { fill:var(--accent); } .spark .s4 { fill:var(--muted); } .spark .s5 { fill:var(--ink); } .spark .line { stroke:var(--accent); } + .spark-axis { @apply mt-1 flex justify-between text-[0.66rem] uppercase tracking-[0.08em]; } + .spark-legend { @apply mt-2 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted; } + .spark-legend li { @apply flex items-center gap-1.5; } + + /* ---------- settings ---------- */ + .settings-form > * + * { @apply mt-5; } + .setting { @apply grid gap-2 border-t border-rule py-3 md:grid-cols-[minmax(0,19rem)_minmax(0,1fr)] md:gap-6; } + .setting:first-of-type { @apply border-t-0 pt-1; } + .setting-label { @apply min-w-0; } + .setting-label label { @apply inline-block; } + .setting-label code { @apply bg-transparent p-0 text-[0.85rem] font-medium text-ink; overflow-wrap:anywhere; } + .setting-input { @apply min-w-0; } + .setting-input > input, .setting-input > select, .setting-input > textarea { @apply w-full max-w-lg; } + .help, .default { @apply mt-1 text-xs text-muted; overflow-wrap:anywhere; } + .setting-label .help { @apply max-w-[42ch]; } + .secret { @apply text-sm text-ink-2; } + code.lines { @apply mt-1 block whitespace-pre-wrap; } + button.reset { @apply ml-1 inline min-h-0 rounded-none border-0 bg-transparent p-0 text-xs text-muted underline decoration-rule underline-offset-2 hover:text-ink; } + .save-bar { @apply sticky bottom-0 z-20 -mx-4 mt-6 flex flex-wrap items-center justify-end gap-x-4 gap-y-2 border-t border-rule bg-paper px-4 py-3 sm:-mx-6 sm:px-6; box-shadow:0 -10px 18px -14px color-mix(in oklab, var(--ink) 55%, transparent); } + .save-bar .meta { @apply mr-auto text-xs; } + @media (max-width:40rem) { .kv { @apply block; } .kv dt { @apply mt-2; } .rating { @apply items-stretch; } .rating-prompt, .rating-note { @apply basis-full; } .rating-prompt { @apply mb-2; } + .filters label { @apply w-full; } + .filters input, .filters select { @apply w-full; } } } diff --git a/src/web/templates/_candidate_row.html b/src/web/templates/_candidate_row.html index b15d19e..612eee7 100644 --- a/src/web/templates/_candidate_row.html +++ b/src/web/templates/_candidate_row.html @@ -1,16 +1,16 @@ -
{{ candidate.title }}{% let signals = candidate.signals %}{% include "_signals_table.html" %}
-{{ candidate.feed }} +
{{ candidate.title }}{% let signals = candidate.signals %}{% include "_signals_table.html" %}
+{{ candidate.feed }} {{ candidate.words }} -{{ candidate.stage }} -{% if let Some(reason) = candidate.reason %}{{ reason }}{% endif %} -{% if let Some(first) = candidate.admitted_first %}{{ first }}{% if !candidate.admitted_rest.is_empty() %} ({{ candidate.admitted_rest }}){% endif %}{% endif %} +{{ candidate.stage }} +{% if let Some(reason) = candidate.reason %}{{ reason }}{% endif %} +{% if let Some(first) = candidate.admitted_first %}{{ first }}{% if !candidate.admitted_rest.is_empty() %} ({{ candidate.admitted_rest }}){% endif %}{% endif %} {{ candidate.utility }} {{ candidate.rank }} {{ candidate.cluster }} {{ candidate.triage }} {{ candidate.quality }} {{ candidate.fit }} -{% if candidate.exploration %}exploration{% endif %}{% if candidate.auto_include %}auto{% endif %} -{% if let Some(why) = candidate.editor_why %}{{ why }}{% endif %} +{% if candidate.exploration %}exploration{% endif %}{% if candidate.auto_include %}auto{% endif %} +{% if let Some(why) = candidate.editor_why %}{{ why }}{% endif %} diff --git a/src/web/templates/_pagination.html b/src/web/templates/_pagination.html index 5fda802..71b828d 100644 --- a/src/web/templates/_pagination.html +++ b/src/web/templates/_pagination.html @@ -1 +1 @@ -{% if pagination.pages() > 1 %}{% endif %} +{% if pagination.pages() > 1 %}{% endif %} diff --git a/src/web/templates/_signals_table.html b/src/web/templates/_signals_table.html index 0f215d0..b67ac1c 100644 --- a/src/web/templates/_signals_table.html +++ b/src/web/templates/_signals_table.html @@ -1,8 +1,8 @@ -{% if signals.empty %}

No signals recorded for this row (hygiene exclusion or thin telemetry).

{% else %}
+{% if signals.empty %}

No signals recorded for this row (hygiene exclusion or thin telemetry).

{% else %}
-{% for line in signals.lines %}{% endfor %}
signalrawnormweightpresent
{{ line.name }}{{ line.raw }}{{ line.norm }}{{ line.weight }}{% if line.present %}yes{% else %}absent{% endif %}
-

Preliminary blend {{ signals.blend }}{% if let Some(cos) = signals.top1_cos %} · interest top-1 cosine {{ cos }}{% endif %}{% if signals.exploration %} · exploration{% endif %}{% if signals.auto_include %} · auto-include{% endif %}

-{% if !signals.top_interests.is_empty() %}

Top interests

    {% for interest in signals.top_interests %}
  • {{ interest.name }} · z {{ interest.z }} · cos {{ interest.cos }}
  • {% endfor %}
{% endif %} -{% if !signals.neighbours.is_empty() %}

Nearest rated neighbours

    {% for neighbour in signals.neighbours %}
  • {{ neighbour.label }} {{ neighbour.title }} · cos {{ neighbour.cos }}
  • {% endfor %}
{% endif %} +{% for line in signals.lines %}{{ line.name }}{{ line.raw }}{{ line.norm }}{{ line.weight }}{% if line.present %}yes{% else %}absent{% endif %}{% endfor %}
+

Preliminary blend {{ signals.blend }}{% if let Some(cos) = signals.top1_cos %} · interest top-1 cosine {{ cos }}{% endif %}{% if signals.exploration %} · exploration{% endif %}{% if signals.auto_include %} · auto-include{% endif %}

+{% if !signals.top_interests.is_empty() %}

Top interests

    {% for interest in signals.top_interests %}
  • {{ interest.name }} · z {{ interest.z }} · cos {{ interest.cos }}
  • {% endfor %}
{% endif %} +{% if !signals.neighbours.is_empty() %}

Nearest rated neighbours

    {% for neighbour in signals.neighbours %}
  • {{ neighbour.label }} {{ neighbour.title }} · cos {{ neighbour.cos }}
  • {% endfor %}
{% endif %} {% if !signals.notes.is_empty() %}
    {% for note in signals.notes %}
  • {{ note }}
  • {% endfor %}
{% endif %}
{% endif %} diff --git a/src/web/templates/dashboard/_pager.html b/src/web/templates/dashboard/_pager.html index 86f3186..51f1d74 100644 --- a/src/web/templates/dashboard/_pager.html +++ b/src/web/templates/dashboard/_pager.html @@ -1 +1 @@ -{% if pager.pages > 1 %}{% else %}

{{ pager.total }} row{% if pager.total != 1 %}s{% endif %}

{% endif %} +{% if pager.pages > 1 %}{% else %}

{{ pager.total }} row{% if pager.total != 1 %}s{% endif %}

{% endif %} diff --git a/src/web/templates/dashboard/_sparkline.html b/src/web/templates/dashboard/_sparkline.html index 00b0ccf..8c13bae 100644 --- a/src/web/templates/dashboard/_sparkline.html +++ b/src/web/templates/dashboard/_sparkline.html @@ -1,8 +1,8 @@
{{ spark.title }}{% if !spark.caption.is_empty() %} · {{ spark.caption }}{% endif %}
-{% if spark.empty %}

No data yet.

{% else %} +{% if spark.empty %}

No data yet.

{% else %} {% for bar in spark.bars %}{{ bar.label }} {% endfor %}{% if !spark.points.is_empty() %}{% endif %}
{{ spark.first_label }}{{ spark.last_label }}
-{% if !spark.legend.is_empty() %}
    {% for item in spark.legend %}
  • {{ item.name }}
  • {% endfor %}
{% endif %} +{% if !spark.legend.is_empty() %}
    {% for item in spark.legend %}
  • {{ item.name }}
  • {% endfor %}
{% endif %} {% endif %}
diff --git a/src/web/templates/dashboard/article.html b/src/web/templates/dashboard/article.html index a3a4629..84d7147 100644 --- a/src/web/templates/dashboard/article.html +++ b/src/web/templates/dashboard/article.html @@ -1,69 +1,81 @@ {% extends "layout.html" %}{% block content %}
-

{{ title }}

-

Article {{ id }} · {{ feed }}{% if let Some(category) = category %} · {{ category }}{% endif %}{% if let Some(author) = author %} · {{ author }}{% endif %}

+
+

Articles › {{ id }}

+

{{ title }}

+

{{ feed }}{% if let Some(category) = category %} · {{ category }}{% endif %}{% if let Some(author) = author %} · {{ author }}{% endif %}

+
{% if let Some(rating) = rating %}{{ rating }}{% else %}unrated{% endif %}Open source
-

Article

+
+

Article

Canonical URL
{{ canonical_url }}
Published
{{ published_at }}
First seen
{{ first_seen }}
Words
{{ words }}{% if excerpt_only %} excerpt only{% endif %}
Images
{{ image_count }}
+
Current rating
{% if let Some(rating) = rating %}{{ rating }}{% else %}unrated{% endif %}
+
+
+

Provenance

+
Sources
{% if sources.is_empty() %}none recorded{% else %}{% for source in sources %}{{ source.kind }} · {{ source.feed }}{% if let Some(category) = source.category %} ({{ category }}){% endif %}{% if !loop.last %}
{% endif %}{% endfor %}{% endif %}
Social
{% if social.is_empty() %}none{% else %}{% for item in social %}{% if let Some(url) = item.url %}{{ item.source }}{% else %}{{ item.source }}{% endif %} · {{ item.score }} points · {{ item.comments }} comments{% if !loop.last %}
{% endif %}{% endfor %}{% endif %}
In issues
{% if in_issues.is_empty() %}never published{% else %}{% for issue in in_issues %}{{ issue.date }} · {{ issue.section }} · position {{ issue.position }}{% if issue.is_lead %} · lead{% endif %}{% if !loop.last %}
{% endif %}{% endfor %}{% endif %}
-
Current rating
{% if let Some(rating) = rating %}{{ rating }}{% else %}unrated{% endif %}
+
+
+ +

Your verdict

{% include "_rating_widget.html" %} -{% if let Some(text) = explain %}
Explain (text)
{{ text }}
{% endif %} +{% if let Some(text) = explain %}
Explain (text)
{{ text }}
{% endif %}

Assessments

-{% if assessments.is_empty() %}

No LLM assessment stored.

{% else %}{% for assessment in assessments %}
-

{{ assessment.stage }}{% if assessment.rejected %} rejected by provider{% endif %}

-{% if assessment.rejected %}

{{ assessment.rationale }}

{{ assessment.model }} · prompt v{{ assessment.prompt_version }} · {{ assessment.assessed_at }}

{% else %}
-{% if assessment.stage == "deep" %}
Quality
{{ assessment.score }}
Fit
{{ assessment.fit }}
Category
{{ assessment.category }}
Format
{{ assessment.kind }}
Paywalled guess
{% if assessment.paywalled %}yes{% else %}no{% endif %}
{% else %}
Interest
{{ assessment.score }}
Kind
{{ assessment.kind }}
{% endif %} +{% if assessments.is_empty() %}

No LLM assessment stored.

{% else %}
{% for assessment in assessments %}
+

{{ assessment.stage }}{% if assessment.rejected %} rejected by provider{% endif %}

+{% if assessment.rejected %}

{{ assessment.rationale }}

{{ assessment.model }} · prompt v{{ assessment.prompt_version }} · {{ assessment.assessed_at }}

{% else %}
+{% if assessment.stage == "deep" %}
Quality
{{ assessment.score }}
Fit
{{ assessment.fit }}
Category
{{ assessment.category }}
Format
{{ assessment.kind }}
Paywalled guess
{% if assessment.paywalled %}yes{% else %}no{% endif %}
{% else %}
Interest
{{ assessment.score }}
Kind
{{ assessment.kind }}
{% endif %}
Rationale
{{ assessment.rationale }}
Model
{{ assessment.model }} · prompt v{{ assessment.prompt_version }} · profile v{{ assessment.profile_version }}
Assessed
{{ assessment.assessed_at }}
-{% if !assessment.facets.is_empty() %}
{% for facet in assessment.facets %}{% endfor %}
facetvalue
{{ facet.name }}{{ facet.value }}
{% endif %}{% endif %} -
{% endfor %}{% endif %} +{% if !assessment.facets.is_empty() %}
{% for facet in assessment.facets %}{% endfor %}
facetvalue
{{ facet.name }}{{ facet.value }}
{% endif %}{% endif %} +
{% endfor %}{% endif %}

Run history

-{% if history.is_empty() %}

Never considered by a run.

{% else %}
+{% if history.is_empty() %}

Never considered by a run.

{% else %}
{% for row in history %} - - - - - + + + + + - +{% endfor %}
rundatestagereasonadmitted byutilityrankclustereditor
run {{ row.run_id }} {{ row.status }}{% let signals = row.signals %}{% include "_signals_table.html" %}
{{ row.date }}{{ row.stage }}{% if let Some(reason) = row.reason %}{{ reason }}{% endif %}{% if let Some(first) = row.admitted_first %}{{ first }}{% if !row.admitted_rest.is_empty() %} ({{ row.admitted_rest }}){% endif %}{% endif %}
run {{ row.run_id }} {{ row.status }}{% let signals = row.signals %}{% include "_signals_table.html" %}
{{ row.date }}{{ row.stage }}{% if let Some(reason) = row.reason %}{{ reason }}{% endif %}{% if let Some(first) = row.admitted_first %}{{ first }}{% if !row.admitted_rest.is_empty() %} ({{ row.admitted_rest }}){% endif %}{% endif %} {{ row.utility }} {{ row.rank }} {{ row.cluster }}{% if let Some(why) = row.editor_why %}{{ why }}{% endif %}{% if let Some(why) = row.editor_why %}{{ why }}{% endif %}
{% endif %}

Neighbours and interests

-{% if let Some(signals) = latest_signals %}{% if signals.top_interests.is_empty() && signals.neighbours.is_empty() %}

The latest run recorded no interests or neighbours for this article.

{% else %}
+{% if let Some(signals) = latest_signals %}{% if signals.top_interests.is_empty() && signals.neighbours.is_empty() %}

The latest run recorded no interests or neighbours for this article.

{% else %}

Top interests

{% if signals.top_interests.is_empty() %}

None.

{% else %}
{% for interest in signals.top_interests %}{% endfor %}
interestzcos
{{ interest.name }}{{ interest.z }}{{ interest.cos }}
{% endif %}
-

Nearest rated neighbours

{% if signals.neighbours.is_empty() %}

None.

{% else %}
{% for neighbour in signals.neighbours %}{% endfor %}
labelcosarticle
{{ neighbour.label }}{{ neighbour.cos }}{{ neighbour.title }}
{% endif %}
-
{% endif %}{% else %}

No signals recorded.

{% endif %} +

Nearest rated neighbours

{% if signals.neighbours.is_empty() %}

None.

{% else %}
{% for neighbour in signals.neighbours %}{% endfor %}
labelcosarticle
{{ neighbour.label }}{{ neighbour.cos }}{{ neighbour.title }}
{% endif %}
+
{% endif %}{% else %}

No signals recorded.

{% endif %}

Embedding

-{% if let Some(embedding) = embedding %}
Model
{{ embedding.model }}
Dimension
{{ embedding.dimension }}
Created
{{ embedding.created_at }}
Input hash
{{ embedding.input_hash }}
{% else %}

No embedding stored.

{% endif %} +{% if let Some(embedding) = embedding %}
Model
{{ embedding.model }}
Dimension
{{ embedding.dimension }}
Created
{{ embedding.created_at }}
Input hash
{{ embedding.input_hash }}
{% else %}

No embedding stored.

{% endif %}

Rating events

-{% if events.is_empty() %}

No rating events.

{% else %}
+{% if events.is_empty() %}

No rating events.

{% else %}
{% for event in events %} - - + + - - - - - + + + + +{% endfor %}
whenlabelvaluekindsourceuserissuenote
{{ event.event_at }}{{ event.label }}{{ event.event_at }}{{ event.label }} {{ event.value }}{{ event.kind }}{{ event.source }}{% if let Some(user) = event.user %}{{ user }}{% else %}{% endif %}{% if let Some(date) = event.issue_date %}{{ date }}{% endif %}{% if let Some(note) = event.note %}{{ note }}{% endif %}{{ event.kind }}{{ event.source }}{% if let Some(user) = event.user %}{{ user }}{% else %}{% endif %}{% if let Some(date) = event.issue_date %}{{ date }}{% endif %}{% if let Some(note) = event.note %}{{ note }}{% endif %}
{% endif %}
{% endblock %} diff --git a/src/web/templates/dashboard/articles.html b/src/web/templates/dashboard/articles.html index 5befdb5..f9f104d 100644 --- a/src/web/templates/dashboard/articles.html +++ b/src/web/templates/dashboard/articles.html @@ -1,7 +1,10 @@ {% extends "layout.html" %}{% block content %}
+

Articles

+

Every article the crawler has ever seen, with the last stage it reached, its scores and its verdict.

+
- + @@ -11,24 +14,24 @@ - Reset +
Reset
{% include "dashboard/_pager.html" %} -
+
{% for article in articles %} - - - + + + - - + + - - -{% endfor %}
first seentitlefeedwordslast stagereasonutilitytriagequalityfitratingpublished
{{ article.first_seen }}{{ article.title }}{% if let Some(feed_id) = article.feed_id %}{{ article.feed }}{% else %}{{ article.feed }}{% endif %}{{ article.first_seen }}{{ article.title }}{% if let Some(feed_id) = article.feed_id %}{{ article.feed }}{% else %}{{ article.feed }}{% endif %} {{ article.words }}{% if let Some(stage) = article.stage %}{{ stage }}{% if let Some(run_id) = article.run_id %} {% if let Some(date) = article.run_date %}{{ date }}{% else %}run {{ run_id }}{% endif %}{% endif %}{% else %}never considered{% endif %}{% if let Some(reason) = article.reason %}{{ reason }}{% endif %}{% if let Some(stage) = article.stage %}{{ stage }}{% if let Some(run_id) = article.run_id %} {% if let Some(date) = article.run_date %}{{ date }}{% else %}run {{ run_id }}{% endif %}{% endif %}{% else %}never considered{% endif %}{% if let Some(reason) = article.reason %}{{ reason }}{% endif %} {{ article.utility }} {{ article.triage }} {{ article.quality }} {{ article.fit }}{% if let Some(rating) = article.rating %}{{ rating }}{% endif %}{% if let Some(date) = article.published %}{{ date }}{% endif %}
+{% if let Some(rating) = article.rating %}{{ rating }}{% endif %} +{% if let Some(date) = article.published %}{{ date }}{% endif %} +{% endfor %}{% if articles.is_empty() %}No articles match this filter.{% endif %} {% include "dashboard/_pager.html" %}
{% endblock %} diff --git a/src/web/templates/dashboard/job.html b/src/web/templates/dashboard/job.html index 4bb1d3b..a3ee747 100644 --- a/src/web/templates/dashboard/job.html +++ b/src/web/templates/dashboard/job.html @@ -1,6 +1,9 @@ {% extends "layout.html" %}{% block content %}
-

Jobs › Job {{ job.id }} · {{ job.name }} {{ job.status }}

-{% if !description.is_empty() %}

{{ description }}

{% endif %} +
+

Jobs › Job {{ job.id }}

+

{{ job.name }} {{ job.status }}

+{% if !description.is_empty() %}

{{ description }}

{% endif %} +
All jobs{% if let Some(run_id) = job.run_id %}Run {{ run_id }}{% endif %}

Job

@@ -12,7 +15,7 @@ {% if let Some(message) = job.message %}
Message
{{ message }}
{% endif %} {% if let Some(run_id) = job.run_id %}
Run
Run {{ run_id }}
{% endif %}
-{% if refresh %}

This page reloads every 5 seconds while the job is requested or running.

{% endif %} +{% if refresh %}

This page reloads every 5 seconds while the job is requested or running.

{% endif %}

Unit

{% if let Some(status) = status %}
@@ -22,11 +25,11 @@ {% if let Some(exited) = status.exited %}
Main exited
{{ exited }}
{% endif %}
{% endif %} {% if let Some(error) = status_error %}

Unit status unavailable: {{ error }}

{% endif %} -

From systemctl show {{ unit }}.

+

From systemctl show {{ unit }}.

-

Log

+

Journal

{% if let Some(error) = log_error %}

Journal unavailable: {{ error }}

{% endif %} -

Last {{ log_lines }} lines of journalctl -u {{ unit }}.

+

Last {{ log_lines }} lines of journalctl -u {{ unit }}.

{{ log }}
{% endblock %} diff --git a/src/web/templates/dashboard/jobs.html b/src/web/templates/dashboard/jobs.html index 6522c9d..d7d6bb5 100644 --- a/src/web/templates/dashboard/jobs.html +++ b/src/web/templates/dashboard/jobs.html @@ -1,28 +1,34 @@ {% extends "layout.html" %}{% block content %}
+

Jobs

-{% if jobs_enabled %}

Each job starts daily-epub-job@<name>.service through systemd; the unit runs daily-epub job run <name> and records itself here. Jobs that take the run lock wait for nothing: a second generate while one is running fails immediately.

{% else %}

Jobs are disabled on this server (server.jobs_enabled = false); run the commands by hand instead.

{% endif %} -
{% for card in cards %}
-

{{ card.name }}

-

{{ card.description }}

-{% if let Some(lock) = card.lock %}

Takes the {{ lock }} lock.

{% endif %} -
-{% if card.dated %}{% endif %} - +

{% if jobs_enabled %}Each job starts daily-epub-job@<name>.service through systemd; the unit runs daily-epub job run <name> and records itself here.{% else %}Jobs are disabled on this server; the catalogue below is read-only.{% endif %}

+
+{% if jobs_enabled %}

Jobs that take the run lock wait for nothing: a second generate while one is running fails immediately.

{% else %}

Jobs are disabled on this server (server.jobs_enabled = false); run the commands by hand instead.

{% endif %} +
{% for card in cards %}
+
+

{{ card.name }}

+{% if let Some(status) = card.last_status %}{{ status }}{% else %}never run{% endif %} +
+

{{ card.description }}

+

{% if let Some(id) = card.last_id %}Last started {% if let Some(when) = card.last_requested %}{{ when }}{% else %}job {{ id }}{% endif %}{% else %}No run recorded yet.{% endif %}{% if let Some(lock) = card.lock %} · takes the {{ lock }} lock{% endif %}

+ +{% if card.dated %}{% endif %} +
{% endfor %}

History

-{% if jobs.is_empty() %}

No jobs recorded yet.

{% else %}
- +{% if jobs.is_empty() %}

No jobs recorded yet.

{% else %}
jobnamerequested byrequestedstartedfinisheddurationstatusmessagerun
+{% for job in jobs %} - - - - - - + + + + + + - - - + + +{% endfor %}
jobnamerequested byrequestedstartedfinisheddurationstatusmessagerun
{{ job.id }}{{ job.name }}{{ job.requested_by }}{{ job.requested }}{{ job.started }}{{ job.finished }}{{ job.id }}{{ job.name }}{{ job.requested_by }}{{ job.requested }}{{ job.started }}{{ job.finished }} {{ job.duration }}{{ job.status }}{% if let Some(message) = job.message %}{{ message }}{% endif %}{% if let Some(run_id) = job.run_id %}{{ run_id }}{% endif %}{{ job.status }}{% if let Some(message) = job.message %}{{ message }}{% endif %}{% if let Some(run_id) = job.run_id %}{{ run_id }}{% endif %}
{% endif %}
{% endblock %} diff --git a/src/web/templates/dashboard/overview.html b/src/web/templates/dashboard/overview.html index 5e5c3ad..5849b84 100644 --- a/src/web/templates/dashboard/overview.html +++ b/src/web/templates/dashboard/overview.html @@ -1,41 +1,55 @@ {% extends "layout.html" %}{% block content %}
+

Overview

+

The state of the paper right now: the last run, today's spend, this week's verdicts and anything waiting on you.

+
+ +
+
Last run{% if let Some(run) = last_run %}{{ run.date }}{{ run.status }} · {{ run.duration }}{% else %}no runs yet{% endif %}
+
Warnings{% if let Some(run) = last_run %}{{ run.warnings }}{% if run.warnings > 0 %}on run {{ run.id }}{% else %}clean run{% endif %}{% else %}no runs yet{% endif %}
+
Verdicts, 7 days{{ ratings_total }}rating history
+
Unrated picks{{ unrated.len() }}from the last three issues
+
Active jobs{{ active_jobs.len() }}all jobs
+
+

Last run

{% if let Some(run) = last_run %}

Run {{ run.id }} · {{ run.date }} · {{ run.status }}

-

Started {{ run.started }} · {{ run.duration }}

+

Started {{ run.started }} · {{ run.duration }}

{% for line in run.lines %}{{ line }}
 {% endfor %}
-{% if run.warnings > 0 %}

{{ run.warnings }} warning{% if run.warnings != 1 %}s{% endif %}

{% endif %} +{% if run.warnings > 0 %}

{{ run.warnings }} warning{% if run.warnings != 1 %}s{% endif %}

{% endif %} {% if let Some(error) = run.error %}

{{ error }}

{% endif %} {% else %}

No runs recorded yet.

{% endif %}

Budget today

-{% if budget.is_empty() %}

No providers configured.

{% else %}
-{% for line in budget %}
{{ line.provider }}
{{ line.spent }} of {{ line.ceiling }}{% if line.over %} ceiling reached{% endif %}
+{% if budget.is_empty() %}

No providers configured.

{% else %}
+{% for line in budget %}
{{ line.provider }}
{{ line.spent }} of {{ line.ceiling }}{% if line.over %} · ceiling reached{% endif %}
{% endfor %}
{% endif %} -

Spend on the current UTC day across every recorded run.

+

Spend on the current UTC day across every recorded run.

Ratings this week

-{% if ratings.is_empty() %}

No explicit ratings in the last seven days.

{% else %}

{{ ratings_total }} verdict{% if ratings_total != 1 %}s{% endif %}: {% for count in ratings %}{{ count.label }} {{ count.count }}{% if !loop.last %} · {% endif %}{% endfor %}

{% endif %} -

Rating history

+{% if ratings.is_empty() %}

No explicit ratings in the last seven days.

{% else %}

{{ ratings_total }} verdict{% if ratings_total != 1 %}s{% endif %}{% for count in ratings %}{{ count.label }} {{ count.count }}{% endfor %}

{% endif %} +

Rating history

Jobs

{% if active_jobs.is_empty() && finished_jobs.is_empty() %}

No jobs recorded.

{% else %} -{% if !active_jobs.is_empty() %}
    {% for job in active_jobs %}
  • {{ job.name }} {{ job.status }} requested {{ job.requested }}
  • {% endfor %}
{% endif %} -{% if !finished_jobs.is_empty() %}

Recently finished

    {% for job in finished_jobs %}
  • {{ job.name }} {{ job.status }} {{ job.finished }}{% if let Some(message) = job.message %} — {{ message }}{% endif %}
  • {% endfor %}
{% endif %} +{% if !active_jobs.is_empty() %}
    {% for job in active_jobs %}
  • {{ job.name }} {{ job.status }} requested {{ job.requested }}
  • {% endfor %}
{% endif %} +{% if !finished_jobs.is_empty() %}

Recently finished

    {% for job in finished_jobs %}
  • {{ job.name }} {{ job.status }} {{ job.finished }}{% if let Some(message) = job.message %} — {{ message }}{% endif %}
  • {% endfor %}
{% endif %} {% endif %} -

All jobs

+

All jobs

{% if !config_warnings.is_empty() %}

Config on disk

-
    {% for line in config_warnings %}
  • {{ line }}
  • {% endfor %}
-

Settings

+
    {% for line in config_warnings %}
  • {{ line }}
  • {% endfor %}
+

Settings

{% endif %}
+

Last 30 runs

{% for item in sparklines %}{% let spark = item %}{% include "dashboard/_sparkline.html" %}{% endfor %}
+

Unrated picks

-{% if unrated.is_empty() %}

Every pick from the last three issues has a verdict.

{% else %}

Picks from the last three issues without a verdict yet.

-
    {% for pick in unrated %}
  • {{ pick.title }} · {{ pick.feed }} · {{ pick.issue_date }} +{% if unrated.is_empty() %}

    Every pick from the last three issues has a verdict.

    {% else %}

    Picks from the last three issues without a verdict yet.

    +
      {% for pick in unrated %}
    • {{ pick.title }} · {{ pick.feed }} · {{ pick.issue_date }} {% let widget = pick.widget %}{% include "_rating_widget.html" %}
    • {% endfor %}
    {% endif %}
{% endblock %} diff --git a/src/web/templates/dashboard/profile.html b/src/web/templates/dashboard/profile.html index e8d1038..a2d7535 100644 --- a/src/web/templates/dashboard/profile.html +++ b/src/web/templates/dashboard/profile.html @@ -1,51 +1,55 @@ {% extends "layout.html" %}{% block content %}
+

Profile

+

The standing taste file the curator reads before every run: what you like, what the OPML declares, and what the editor model has learned from your verdicts.

+
-
-
-

profile.md

-

{{ path }}{% if !exists %} — missing; saving creates it{% else %} · {{ bytes }} bytes{% endif %} · limit {{ max_bytes }} bytes. Any ## Interests section is parsed one interest per line; everything else goes into the system prompt verbatim.

-
- -
The next run rebuilds the system prompt from the saved file.
+
+
+

profile.md

+

{{ path }}{% if !exists %} — missing; saving creates it{% else %} · {{ bytes }} bytes{% endif %} · limit {{ max_bytes }} bytes. Any ## Interests section is parsed one interest per line; everything else goes into the system prompt verbatim.

+ + +
The next run rebuilds the system prompt from the saved file.
-
-

What the loader parses

-

Passthrough sections

-{% if preview_body.trim().is_empty() %}

Nothing passes through — the file is empty or only has an Interests section.

{% else %}
{{ preview_body }}
{% endif %} -

Extracted ## Interests lines

-{% if preview_interests.is_empty() %}

None. The prompt uses the OPML interests alone.

{% else %}
    {% for interest in preview_interests %}
  • {{ interest }}
  • {% endfor %}
{% endif %} +
+

What the loader parses

+

A live read of the text on the left, exactly as curate::profile splits it.

+

Passthrough sections

+{% if preview_body.trim().is_empty() %}

Nothing passes through — the file is empty or only has an Interests section.

{% else %}
{{ preview_body }}
{% endif %} +

Extracted ## Interests lines

+{% if preview_interests.is_empty() %}

None. The prompt uses the OPML interests alone.

{% else %}
    {% for interest in preview_interests %}
  • {{ interest }}
  • {% endfor %}
{% endif %}

History

-{% if versions.is_empty() %}

No saved versions yet. The first save records the current file as version 1.

{% else %} -

Each row is the text that was replaced by a save (or a restore). Restore writes it back and records what is on disk now as a new version.

-
- +{% if versions.is_empty() %}

No saved versions yet. The first save records the current file as version 1.

{% else %} +

Each row is the text that was replaced by a save (or a restore). Restore writes it back and records what is on disk now as a new version.

+
VersionReplaced atBySizePreview
+{% for version in versions %} - - - - + + + + - +{% endfor %}
VersionReplaced atBySizePreview
#{{ version.id }}{{ version.saved_at }}{{ version.saved_by }}{{ version.bytes }} bytes#{{ version.id }}{{ version.saved_at }}{{ version.saved_by }}{{ version.bytes }} bytes
{{ version.preview }}
{% endif %}

Standing interests

-

{{ opml_path }} · {{ opml_count }} interests, grouped the way the system prompt lists them. The union of these and the ## Interests lines above is what the prompt uses; edit the OPML file to change them.

-{% if !opml_error.is_empty() %}

! {{ opml_error }}

{% endif %} -{% if !themes.is_empty() %}
{% for theme in themes %}
{{ theme.name }} ({{ theme.count }})
{{ theme.members }}
{% endfor %}
{% endif %} +

{{ opml_path }} · {{ opml_count }} interests, grouped the way the system prompt lists them. The union of these and the ## Interests lines above is what the prompt uses; edit the OPML file to change them.

+{% if !opml_error.is_empty() %}

{{ opml_error }}

{% endif %} +{% if !themes.is_empty() %}
{% for theme in themes %}
{{ theme.name }} ({{ theme.count }})
{{ theme.members }}
{% endfor %}
{% endif %}

Learned adjustments

-

Rebuilt weekly from ratings by the editor model (prompt version {{ prompt_version }}, built {{ prompt_built_at }}, {{ learned_age }}). {% if rebuild_due %}A rebuild is due — the next run performs it, or start it now.{% else %}The next scheduled rebuild is at least {{ rebuild_interval_days }} days after the last one; the next run performs it when due.{% endif %}

-{% if learned.trim().is_empty() %}

No learned adjustments stored yet.

{% else %}
{{ learned }}
{% endif %} -
{% if !jobs_enabled %} Jobs are disabled on this server (server.jobs_enabled = false); run daily-epub profile rebuild instead.{% else %} Starts the profile-rebuild job through systemd.{% endif %}
+

Rebuilt weekly from ratings by the editor model (prompt version {{ prompt_version }}, built {{ prompt_built_at }}, {{ learned_age }}). {% if rebuild_due %}A rebuild is due — the next run performs it, or start it now.{% else %}The next scheduled rebuild is at least {{ rebuild_interval_days }} days after the last one; the next run performs it when due.{% endif %}

+{% if learned.trim().is_empty() %}

No learned adjustments stored yet.

{% else %}
{{ learned }}
{% endif %} +
{% if !jobs_enabled %} Jobs are disabled on this server (server.jobs_enabled = false); run daily-epub profile rebuild instead.{% else %} Starts the profile-rebuild job through systemd.{% endif %}

System prompt

-

Stored after the last run: version {{ prompt_version }}, {{ prompt_chars }} characters, {{ prompt_verdicts }} verdict lines. Every LLM call of a run receives exactly this text.

-{% if prompt.is_empty() %}

No prompt has been built yet.

{% else %}
Show the system prompt
{{ prompt }}
{% endif %} +

Stored after the last run: version {{ prompt_version }}, {{ prompt_chars }} characters, {{ prompt_verdicts }} verdict lines. Every LLM call of a run receives exactly this text.

+{% if prompt.is_empty() %}

No prompt has been built yet.

{% else %}
Show the system prompt
{{ prompt }}
{% endif %}
{% endblock %} diff --git a/src/web/templates/dashboard/ratings.html b/src/web/templates/dashboard/ratings.html index e7c2762..ef8282e 100644 --- a/src/web/templates/dashboard/ratings.html +++ b/src/web/templates/dashboard/ratings.html @@ -1,19 +1,23 @@ {% extends "layout.html" %}{% block content %}
+

Ratings

-

{{ summary_line }}

+

{{ summary_line }}

+
{% if no_embedding_count > 0 %}

{{ no_embedding_count }} rated articles have no embedding and cannot act as neighbours — run the features-backfill job (features backfill --rated-only).

{% endif %} -
+
How ratings enter the algorithm -
    +
    +
    1. Prompt verdict block. The {{ how.verdicts_in_prompt }} most recent non-cleared verdicts, newest first, are written into every LLM call's system prompt as one line each (LOVED | title | feed | summary). Only the rank matters here — a verdict never ages out of this block, it is pushed out by newer ones. Tune curation.feedback.verdicts_in_prompt.
    2. Weekly learned adjustments. Every {{ how.rebuild_interval_days }} days the editor model rewrites the profile's "Learned adjustments" bullets from the {{ how.max_ratings_in_rebuild }} most recent non-cleared verdicts, including notes and deep-assessment facets. See the Profile page.
    3. Rated-neighbour signal. Each verdict with an embedding is an example with weight value × 0.5^(age / {{ how.half_life_days }} days), where loved = {{ how.loved }}, good = {{ how.good }}, not for me = {{ how.not_for_me }}; ratings older than {{ how.lookback_days }} days are not loaded. A candidate's signal is the weighted mean cosine to its {{ how.neighbour_k }} nearest positive examples minus {{ how.negative_coefficient }} × the same over its nearest negative ones. The signal's preliminary weight ({{ how.knn_weight }}) is scaled by a gate that opens above {{ how.knn_floor }} embedded verdicts and is fully open at {{ how.knn_full }}. Tune curation.ranking.rating_half_life_days, knn_floor, knn_full, neighbour_k and weights.preliminary.knn.
    4. Feed affinity. The same decayed weight is credited to the rated article's direct feeds, split evenly; each feed's Beta-smoothed rate (up + 1) / (up + down + 2) becomes a candidate's signal (the mean over its rated direct feeds). Its weight ({{ how.feed_weight }}) is gated between {{ how.feed_floor }} and {{ how.feed_full }} attributable ratings. Tune curation.ranking.feed_floor, feed_full and weights.preliminary.feed.
    -

    Clearing a verdict removes it from all four paths without deleting history; ratings are append-only.

    +

    Clearing a verdict removes it from all four paths without deleting history; ratings are append-only.

    +
- + {% if tab == "events" %}
@@ -23,27 +27,27 @@ - +
Reset
-

{{ pagination.total }} events, newest first. History is append-only; a later explicit event for the same article marks the earlier one superseded. Use the clear verdict to retract.

-
- +

{{ pagination.total }} events, newest first. History is append-only; a later explicit event for the same article marks the earlier one superseded. Use the clear verdict to retract.

+
EventVerdictValueArticleIssueWhenSourceByNote
+ {% for event in events %} - - - - - - - - - + + + + + + + + + {% endfor %} -{% if events.is_empty() %}{% endif %} +{% if events.is_empty() %}{% endif %}
EventVerdictValueArticleIssueWhenSourceByNote
#{{ event.id }}{% if event.kind != "explicit" %} {{ event.kind }}{% endif %}{% if event.superseded %} superseded{% endif %}{{ event.verdict }}{{ event.value }}{{ event.title }}{% if !event.issue_date.is_empty() %}{{ event.issue_date }}{% endif %}{{ event.when }}{{ event.source }}{{ event.username }}{{ event.note }}#{{ event.id }}{% if event.kind != "explicit" %} {{ event.kind }}{% endif %}{% if event.superseded %} superseded{% endif %}{{ event.verdict }}{{ event.value }}{{ event.title }}{% if !event.issue_date.is_empty() %}{{ event.issue_date }}{% endif %}{{ event.when }}{{ event.source }}{{ event.username }}{{ event.note }}
No rating events match.
No rating events match.
-
{% if !prev_href.is_empty() %}← Newer{% endif %} {% include "_pagination.html" %} {% if !next_href.is_empty() %}Older →{% endif %}
+ {% else %}
@@ -51,26 +55,26 @@ - +
Reset
-

One row per rated article (its latest explicit event), newest first. {% match last_run %}{% when Some with (run) %}"Used last run" counts the candidates of run #{{ run.id }} ({{ run.date }}, {{ run.status }}) that listed the article among their nearest rated neighbours, and how many of those were selected.{% when None %}No run has happened yet, so "Used last run" is empty.{% endmatch %}

-
- +

One row per rated article (its latest explicit event), newest first. {% match last_run %}{% when Some with (run) %}"Used last run" counts the candidates of run #{{ run.id }} ({{ run.date }}, {{ run.status }}) that listed the article among their nearest rated neighbours, and how many of those were selected.{% when None %}No run has happened yet, so "Used last run" is empty.{% endmatch %}

+
VerdictArticleWhen · byNoteAge → decayNeighbour weightFeed creditIn promptIn rebuildUsed last run
+ {% for row in current %} - - - - - - - - - - + + + + + + + + + + {% endfor %} -{% if current.is_empty() %}{% endif %} +{% if current.is_empty() %}{% endif %}
VerdictArticleWhen · byNoteAge → decayNeighbour weightFeed creditIn promptIn rebuildUsed last run
{{ row.verdict }}{% let widget = row.widget %}{% include "_rating_widget.html" %}{{ row.title }}
{{ row.feed_title }}{% if !row.issue_date.is_empty() %} · {{ row.issue_date }}{% endif %}
{{ row.when }}
{{ row.source }} · {{ row.username }}
{{ row.note }}{{ row.age_days }} d → {{ row.decay }}{% if row.has_embedding && !row.neighbour_weight.is_empty() %}{{ row.neighbour_weight }}{% if row.beyond_lookback %} (beyond lookback){% endif %}{% else if row.badge == "cleared" %}{% else %}no embedding{% endif %}{% for credit in row.feed_credits %}{{ credit.title }} {{ credit.credit }}{% if !loop.last %}
{% endif %}{% endfor %}
{% if row.in_prompt %}✓{% endif %}{% if row.in_rebuild %}✓{% endif %}{% if row.used_total > 0 %}{{ row.used_total }} ({{ row.used_selected }} selected){% endif %}{{ row.verdict }}{% let widget = row.widget %}{% include "_rating_widget.html" %}{{ row.title }}
{{ row.feed_title }}{% if !row.issue_date.is_empty() %} · {{ row.issue_date }}{% endif %}
{{ row.when }}
{{ row.source }} · {{ row.username }}
{{ row.note }}{{ row.age_days }} d → {{ row.decay }}{% if row.has_embedding && !row.neighbour_weight.is_empty() %}{{ row.neighbour_weight }}{% if row.beyond_lookback %} (beyond lookback){% endif %}{% else if row.badge == "cleared" %}{% else %}no embedding{% endif %}{% for credit in row.feed_credits %}{{ credit.title }} {{ credit.credit }}{% if !loop.last %}
{% endif %}{% endfor %}
{% if row.in_prompt %}{% endif %}{% if row.in_rebuild %}{% endif %}{% if row.used_total > 0 %}{{ row.used_total }} ({{ row.used_selected }} selected){% endif %}
No current verdicts match.
No current verdicts match.
{% endif %}
{% endblock %} diff --git a/src/web/templates/dashboard/run.html b/src/web/templates/dashboard/run.html index 4d7d2be..37a957c 100644 --- a/src/web/templates/dashboard/run.html +++ b/src/web/templates/dashboard/run.html @@ -1,42 +1,51 @@ {% extends "layout.html" %}{% block content %}
-

Run {{ run.id }} · {{ run.date }} {{ run.status }}{% if run.dry_run %} dry run{% endif %}

-

Started {{ run.started }} · finished {{ run.finished }} · {{ run.duration }} · {{ run.total_cost }}

- +
+

Runs › {{ run.date }}

+

Run {{ run.id }} {{ run.status }}{% if run.dry_run %} dry run{% endif %}

+

Started {{ run.started }} · finished {{ run.finished }}.

+
+ +
+
Issue date{{ run.date }}run {{ run.id }}
+
Duration{{ run.duration }}wall clock
+
Token cost{{ run.total_cost }}every provider
+
Warnings{{ warnings.len() }}{% if warnings.is_empty() %}clean{% else %}see below{% endif %}
+
{% if let Some(error) = run.error %}

{{ error }}

{% endif %}

Funnel

{% for stage in funnel %} - + - - - + + +{% endfor %}
stagereachedreachedstopped herewhy
{{ stage.stage }}{{ stage.stage }}
{{ stage.reached }}{{ stage.stopped }}{% for reason in stage.reasons %}{{ reason.reason }} {{ reason.count }}{% if !loop.last %} · {% endif %}{% endfor %}{{ stage.reached }}{{ stage.stopped }}{% for reason in stage.reasons %}{{ reason.reason }} {{ reason.count }}{% if !loop.last %} · {% endif %}{% endfor %}
{% if has_report %}
-

Admission mix

{% if admission.is_empty() %}

Nothing admitted.

{% else %}
{% for line in admission %}
{{ line.name }}
{{ line.count }}
{% endfor %}
{% endif %}
+

Admission mix

{% if admission.is_empty() %}

Nothing admitted.

{% else %}
{% for line in admission %}
{{ line.name }}
{{ line.count }}
{% endfor %}
{% endif %}

Preference state

-
rated with embeddings
{{ preference.rated_with_embeddings }}
-
knn gate
{{ preference.knn_gate }}
-
feed gate
{{ preference.feed_gate }}
-
verdicts in prompt
{{ preference.verdicts_in_prompt }}
+
rated with embeddings
{{ preference.rated_with_embeddings }}
+
knn gate
{{ preference.knn_gate }}
+
feed gate
{{ preference.feed_gate }}
+
verdicts in prompt
{{ preference.verdicts_in_prompt }}

Timings

{% for line in timings %}{% endfor %}
stageseconds
{{ line.stage }}{{ line.seconds }}
total{{ timings_total }}

Provider usage

{% for line in providers %}{% endfor %}
providerincachedcache writeoutcost
{{ line.provider }}{{ line.input }}{{ line.cached }}{{ line.cache_write }}{{ line.output }}{{ line.cost }}
-

Warnings

{% if warnings.is_empty() %}

None.

{% else %}
    {% for warning in warnings %}
  • {{ warning }}
  • {% endfor %}
{% endif %}
+

Warnings

{% if warnings.is_empty() %}

None.

{% else %}
    {% for warning in warnings %}
  • {{ warning }}
  • {% endfor %}
{% endif %}

Feeds (top {{ feeds.len() }})

{% for line in feeds %}{% endfor %}
feedentries
{{ line.name }}{{ line.count }}
-
{% else %}

This run has no stored report; only the funnel and candidates are available.

{% endif %} +{% else %}

This run has no stored report; only the funnel and candidates are available.

{% endif %}

Config diff

-{% if let Some(against) = diff_against %}{% if config_diff.is_empty() %}

No settings changed since run {{ against }}.

{% else %}

Against the previous non-dry run, run {{ against }}.

-
{% for row in config_diff %}{% endfor %}
keybeforeafter
{{ row.key }}{{ row.before }}{{ row.after }}
{% endif %}{% else %}

No earlier non-dry run to compare against.

{% endif %} +{% if let Some(against) = diff_against %}{% if config_diff.is_empty() %}

No settings changed since run {{ against }}.

{% else %}

Against the previous non-dry run, run {{ against }}.

+
{% for row in config_diff %}{% endfor %}
keybeforeafter
{{ row.key }}{{ row.before }}{{ row.after }}
{% endif %}{% else %}

No earlier non-dry run to compare against.

{% endif %}

Near misses

-{% if near_misses.is_empty() %}

None recorded.

{% else %}
- -{% for miss in near_misses %}{% endfor %}
#scoretitlefeedqualityfitstagereason
{{ loop.index }}{{ miss.score }}{{ miss.title }}{{ miss.feed }}{{ miss.quality }}{{ miss.fit }}{{ miss.stage }}{% if let Some(reason) = miss.reason %}{{ reason }}{% endif %}
{% endif %} +{% if near_misses.is_empty() %}

None recorded.

{% else %}
+ +{% for miss in near_misses %}{% endfor %}
#scoretitlefeedqualityfitstagereason
{{ loop.index }}{{ miss.score }}{{ miss.title }}{{ miss.feed }}{{ miss.quality }}{{ miss.fit }}{{ miss.stage }}{% if let Some(reason) = miss.reason %}{{ reason }}{% endif %}
{% endif %}

Candidates

@@ -44,13 +53,13 @@ - + - Reset +
Reset
{% include "dashboard/_pager.html" %} -
+
-{% for candidate in candidates %}{% include "_candidate_row.html" %}{% endfor %}
titlefeedwordsstagereasonadmitted byutilityrankclustertriagequalityfitflagseditor
+{% for candidate in candidates %}{% include "_candidate_row.html" %}{% endfor %}{% if candidates.is_empty() %}No candidates match this filter.{% endif %} {% include "dashboard/_pager.html" %}
{% endblock %} diff --git a/src/web/templates/dashboard/runs.html b/src/web/templates/dashboard/runs.html index b7b02a4..f5b0321 100644 --- a/src/web/templates/dashboard/runs.html +++ b/src/web/templates/dashboard/runs.html @@ -1,21 +1,24 @@ {% extends "layout.html" %}{% block content %}
+

Runs

+

Every recorded pipeline run, newest first: how the funnel narrowed, what it cost and whether it finished clean.

+
- +
{% if !status.is_empty() %}Reset{% endif %}
-
- +
rundatestatusstarteddurationconsidered → eligible → triaged → assessed → shortlisted → selectedcosttotalwarnings
+{% for run in runs %} - - - - - - - + + + + + + + - -{% endfor %}
rundatestatusstarteddurationconsidered → eligible → triaged → assessed → shortlisted → selectedcosttotalwarnings
{{ run.id }}{{ run.date }}{{ run.status }}{% if run.dry_run %} dry{% endif %}{{ run.started }}{{ run.duration }}{{ run.funnel }}{{ run.costs }}{{ run.id }}{{ run.date }}{{ run.status }}{% if run.dry_run %} dry{% endif %}{{ run.started }}{{ run.duration }}{{ run.funnel }}{{ run.costs }} {{ run.total }}{% if run.warnings > 0 %}{{ run.warnings }}{% else %}0{% endif %}
+{% if run.warnings > 0 %}{{ run.warnings }}{% else %}0{% endif %} +{% endfor %}{% if runs.is_empty() %}No runs match this filter.{% endif %} {% include "dashboard/_pager.html" %}
{% endblock %} diff --git a/src/web/templates/dashboard/settings.html b/src/web/templates/dashboard/settings.html index 443f4a4..43cd817 100644 --- a/src/web/templates/dashboard/settings.html +++ b/src/web/templates/dashboard/settings.html @@ -1,15 +1,21 @@ {% extends "layout.html" %}{% block content %}
+

Settings

-

{% match config_path %}{% when Some with (path) %}Editing {{ path }}. Saved settings apply to the next run; a hand edit on disk shows up on the next page view.{% when None %}No config file is configured; start the server with --config to edit settings here. The values below are the built-in defaults plus the environment.{% endmatch %} · History

-{% match load_error %}{% when Some with (error) %}{% when None %}{% endmatch %} +

{% match config_path %}{% when Some with (path) %}Editing {{ path }}. Saved settings apply to the next run; a hand edit on disk shows up on the next page view.{% when None %}No config file is configured; start the server with --config to edit settings here. The values below are the built-in defaults plus the environment.{% endmatch %}

+
+{% match load_error %}{% when Some with (error) %}{% when None %}{% endmatch %} {% if !errors.is_empty() %}{% endif %} +
-{% for group in groups %}
-

{{ group.title }}

-{% match group.provider %}{% when Some with (provider) %}

Key: {{ crate::config::ProviderConfig::api_key_env_var(provider.name) }} in the env file. {% if provider.removable() %}Not named by any [llm] role. {% else if provider.built_in %}Built in: declared by the defaults, so it can be edited and left unreferenced but not removed.{% else %}Named by llm.{{ provider.roles() }}; reassign the role before removing it.{% endif %}

{% when None %}{% endmatch %} -{% if group.is_weights() %}

Weights need not sum to 1; they are renormalized over the signals present for each article.

{% endif %} +{% for group in groups %}
+
+

{{ group.id }}

+

{{ group.title }}

+
+{% match group.provider %}{% when Some with (provider) %}

Key: {{ crate::config::ProviderConfig::api_key_env_var(provider.name) }} in the env file. {% if provider.removable() %}Not named by any [llm] role. {% else if provider.built_in %}Built in: declared by the defaults, so it can be edited and left unreferenced but not removed.{% else %}Named by llm.{{ provider.roles() }}; reassign the role before removing it.{% endif %}

{% when None %}{% endmatch %} +{% if group.is_weights() %}

Weights need not sum to 1; they are renormalized over the signals present for each article.

{% endif %} {% for field in group.fields %}
-
{% if field.restart_required %} restart{% endif %}{% match field.help %}{% when Some with (help) %}

{{ help }}

{% when None %}{% endmatch %}
+
{% if field.restart_required %} restart{% endif %}{% if field.is_env() %} locked{% endif %}{% match field.help %}{% when Some with (help) %}

{{ help }}

{% when None %}{% endmatch %}
{% if field.is_secret() %}

{% if field.is_set() %}set{% if field.is_env() %} (from {{ field.env_var() }}){% else if field.is_file() %} (in the config file — move it to the env file){% endif %}{% else %}not set — set {{ field.env_var() }} in the env file{% endif %}

{% else if field.is_env() %}{% if field.is_text_list() %}{% else %}{% endif %}

locked by {{ field.env_var() }} in the env file

@@ -25,17 +31,17 @@ {% endfor %}
{% endfor %} -
{% match config_path %}{% when Some with (_path) %}{% when None %}{% endmatch %}
+
{% match config_path %}{% when Some with (path) %}Writes {{ path }}.{% when None %}No config file: settings are read-only.{% endmatch %}
-{% for group in groups %}{% match group.provider %}{% when Some with (provider) %}{% if provider.removable() %}
{% endif %}{% when None %}{% endmatch %}{% endfor %} -
+{% for group in groups %}{% match group.provider %}{% when Some with (provider) %}{% if provider.removable() %}
{% endif %}{% when None %}{% endmatch %}{% endfor %} +

Add provider

-
+ -

Inserts [providers.<name>] with placeholder base_url and model; edit them above afterwards and put DAILY_EPUB_PROVIDERS__<NAME>__API_KEY in the env file.

- +
+

Inserts [providers.<name>] with placeholder base_url and model; edit them above afterwards and put DAILY_EPUB_PROVIDERS__<NAME>__API_KEY in the env file.

{% endblock %} diff --git a/src/web/templates/dashboard/settings_history.html b/src/web/templates/dashboard/settings_history.html index e55fbce..209b6d3 100644 --- a/src/web/templates/dashboard/settings_history.html +++ b/src/web/templates/dashboard/settings_history.html @@ -1,18 +1,18 @@ {% extends "layout.html" %}{% block content %}
+

Settings history

-

Settings · every change made through the dashboard, newest first.

-{% if changes.is_empty() %}

No settings have been changed through the dashboard yet.

{% else %}
+

Every settings change made through the dashboard, newest first. Hand edits to config.toml on disk are not recorded here.

+ +{% if changes.is_empty() %}

No settings have been changed through the dashboard yet.

{% else %}
{% for change in changes %} - - + + - - + +{% endfor %}
WhenWhoKeyBeforeAfter
{{ change.changed_at }}{% match change.username %}{% when Some with (name) %}{{ name }}{% when None %}removed user{% endmatch %}{{ change.changed_at }}{% match change.username %}{% when Some with (name) %}{{ name }}{% when None %}removed user{% endmatch %} {{ change.key }}{% match change.old_value %}{% when Some with (value) %}
{{ value }}
{% when None %}absent{% endmatch %}
{% match change.new_value %}{% when Some with (value) %}
{{ value }}
{% when None %}removed{% endmatch %}
{% match change.old_value %}{% when Some with (value) %}
{{ value }}
{% when None %}absent{% endmatch %}
{% match change.new_value %}{% when Some with (value) %}
{{ value }}
{% when None %}removed{% endmatch %}
-{% include "_pagination.html" %} -{% if pagination.page < pagination.pages() %}

Older

{% endif %} -{% if pagination.page > 1 %}

Newer

{% endif %} +{% if pagination.pages() > 1 %}{% endif %} {% endif %}
{% endblock %} diff --git a/src/web/templates/dashboard/stats.html b/src/web/templates/dashboard/stats.html index 4af49c2..1229f5c 100644 --- a/src/web/templates/dashboard/stats.html +++ b/src/web/templates/dashboard/stats.html @@ -1,31 +1,40 @@ {% extends "layout.html" %}{% block content %}
+

Stats

-

Last {{ days }} days ({{ since_date }} → {{ today }}) · window: {% for window in windows %}{% if *window == days %}{{ window }}{% else %}{{ window }}{% endif %}{% if !loop.last %} · {% endif %}{% endfor %} days · the same figures as daily-epub stats --days {{ days }}.

+

The last {{ days }} days ({{ since_date }} → {{ today }}) — the same figures as daily-epub stats --days {{ days }}.

+
+ +
{% for line in summary %}
{{ line.key }}{{ line.value }}
{% endfor %}
+
{% for item in sparklines %}{% let spark = item %}{% include "dashboard/_sparkline.html" %}{% endfor %}
+
-

Issues and ratings

-
{% for line in summary %}
{{ line.key }}
{{ line.value }}
{% endfor %}
-{% if ratings.is_empty() %}

No explicit ratings in the window.

{% else %}

{% for line in ratings %}{{ line.key }} {{ line.value }}{% if !loop.last %} · {% endif %}{% endfor %}

{% endif %} +

Verdicts in the window

+{% if ratings.is_empty() %}

No explicit ratings in the window.

{% else %}

{% for line in ratings %}{{ line.key }} {{ line.value }}{% endfor %}

{% endif %} +

Explicit verdicts only; cleared ones still count as events.

Retriever yield

{% if retrievers.is_empty() %}

No rated picks by admitting retriever in the window.

{% else %}
-{% for line in retrievers %}{% endfor %}
admitted byratedupdownratio
{{ line.retriever }}{{ line.rated }}{{ line.up }}{{ line.down }}{{ line.ratio }}
{% endif %} -

Exploration

-
{% for line in exploration %}
{{ line.key }}
{{ line.value }}
{% endfor %}
+{% for line in retrievers %}{{ line.retriever }}{{ line.rated }}{{ line.up }}{{ line.down }}{{ line.ratio }}{% endfor %}
{% endif %} +

Exploration

+
{% for line in exploration %}
{{ line.key }}
{{ line.value }}
{% endfor %}

Cost per day

-
{% for line in cost_per_day %}
{{ line.key }}
{{ line.value }}
{% endfor %}
-

Window spend divided by {{ days }} days, per provider.

+
{% for line in cost_per_day %}
{{ line.key }}
{{ line.value }}
{% endfor %}
+

Window spend divided by {{ days }} days, per provider.

+

Spend by day

-{% if cost_rows.is_empty() %}

No provider costs recorded in the window.

{% else %}
+{% if cost_rows.is_empty() %}

No provider costs recorded in the window.

{% else %}
{% for provider in providers %}{% endfor %} -{% for row in cost_rows %}{% for cell in row.cells %}{% endfor %}{% endfor %}
day (UTC){{ provider }}total
{{ row.date }}{{ cell }}{{ row.total }}
{% endif %} +{% for row in cost_rows %}{{ row.date }}{% for cell in row.cells %}{{ cell }}{% endfor %}{{ row.total }}{% endfor %}{% endif %} +

Runs in the window

-{% if runs.is_empty() %}

No finished runs in the window.

{% else %}
- -{% for run in runs %}{% endfor %}
rundatestatuscostselectedduration
{{ run.run_id }}{{ run.date }}{{ run.status }}{{ run.cost }}{{ run.selected }}{{ run.duration }}
{% endif %} -
As text (daily-epub stats --days {{ days }})
{{ text }}
+{% if runs.is_empty() %}

No finished runs in the window.

{% else %}
+ +{% for run in runs %}{% endfor %}
rundatestatuscostselectedduration
{{ run.run_id }}{{ run.date }}{{ run.status }}{{ run.cost }}{{ run.selected }}{{ run.duration }}
{% endif %} + +
As text (daily-epub stats --days {{ days }})
{{ text }}
{% endblock %} diff --git a/src/web/templates/dashboard/users.html b/src/web/templates/dashboard/users.html index 8ca09b6..6cf54a9 100644 --- a/src/web/templates/dashboard/users.html +++ b/src/web/templates/dashboard/users.html @@ -1,15 +1,18 @@ {% extends "layout.html" %}{% block content %}
+

Users

-

Accounts are read-only here. Create, change, disable, enable, or sign out users with the daily-epub users CLI on the server.

-
+

Every account on this server, with its role and open sessions.

+ +

Accounts are read-only here. Create, change, disable, enable, or sign out users with the daily-epub users CLI on the server.

+
{% for user in users %} - + - - + + -{% endfor %}{% if users.is_empty() %}{% endif %} +{% endfor %}{% if users.is_empty() %}{% endif %}
UsernameRoleStatusCreatedLast loginOpen sessions
{{ user.username }}{{ user.username }} {{ user.role }} {% if user.disabled %}disabled{% else %}enabled{% endif %}{{ user.created }}{{ user.last_login }}{{ user.created }}{{ user.last_login }} {{ user.open_sessions }}
No users. Bootstrap an admin with daily-epub users add <username> --admin.
No users. Bootstrap an admin with daily-epub users add <username> --admin.
{% endblock %}