Files
sortof/frontend/index.html

1434 lines
45 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>sortof (:|) sorted. or close enough.</title>
<link rel="icon" type="image/svg+xml" href="/img/ketchup_bottle.svg">
<link rel="alternate icon" type="image/png" href="/img/ketchup_bottle.png">
<link rel="apple-touch-icon" href="/img/ketchup_bottle.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Sora used throughout per brief; JetBrains Mono only for code/output blocks. -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap">
<style>
/* ─── tokens ─────────────────────────────────────────────── */
:root {
/* ─── LOCKED PALETTE — Indifferent Ketchup brief 2026-05-01 ─── */
--bg: #0A141E;
--bg-surface: #14293C;
--bg-elevated: #1B3550;
--border: #2A4A6B;
--fg: #FFFFFF;
--fg-muted: #A3B8CC;
/* Brand color matches the ketchup bottle: vivid red, comparable punch
* to the previous neon green. --brand-ink stays dark for AA contrast
* on the red fill. */
--brand: #FF3838; /* IK ketchup red */
--brand-ink: #0A141E;
--brand-hover: #E62E2E;
--secondary: #0050FF; /* IK blue accent */
--secondary-ink: #FFFFFF;
--info: #56B4E9; --info-ink: #0A141E;
--success: var(--brand); --success-ink: var(--brand-ink);
--warning: #E69F00; --warning-ink: #0A141E;
/* --error: brief specified #D55E00, lifted to #E97515 — same hue ~30°,
* higher luminance — so pill text on --bg-surface clears WCAG AA
* (#D55E00 was 3.85:1, fails 4.5:1; #E97515 is 4.62:1). Hue preserved
* per brief rule "Adjust luminance, not hue, if a token fails." */
--error: #E97515; --error-ink: #FFFFFF;
--focus-ring: #56B4E9; /* same as --info per brief */
/* Surface variants of state colors (12-16% alpha) for chip backgrounds. */
--info-bg: rgb(86 180 233 / 0.14);
--success-bg: rgb(255 56 56 / 0.14); /* matches new --brand (#FF3838) */
--warning-bg: rgb(230 159 0 / 0.14);
--error-bg: rgb(233 117 21 / 0.16);
/* ─── Aliases — old token names map to the core palette so existing
* component CSS keeps working without selector-by-selector edits. */
--bg-panel: var(--bg-surface);
--bg-1: var(--bg-surface);
--bg-2: var(--bg-surface);
--bg-3: var(--bg-surface);
--bg-hi: var(--bg-elevated);
--line: var(--border);
--line-2: var(--border);
--fg-1: var(--fg);
--fg-2: var(--fg-muted);
--fg-3: var(--fg-muted);
--acc-success: var(--success);
--acc-warn: var(--warning);
--acc-error: var(--error);
--acc-info: var(--info);
--acc-success-bg: var(--success-bg);
--acc-warn-bg: var(--warning-bg);
--acc-error-bg: var(--error-bg);
--acc-info-bg: var(--info-bg);
--acc-green: var(--success);
--acc-amber: var(--warning);
--acc-red: var(--error);
--acc-blue: var(--info);
--acc-green-bg: var(--success-bg);
--acc-amber-bg: var(--warning-bg);
--acc-red-bg: var(--error-bg);
--acc-blue-bg: var(--info-bg);
--brand-primary: var(--brand);
--brand-primary-bg: rgb(255 56 56 / 0.14);
--brand-anchor-bg: var(--brand-ink);
--brand-shadow-card: 0 4px 12px rgba(0, 0, 0, 0.45);
--radius: 8px;
--radius-sm: 4px;
--radius-lg: 12px;
--pad-x: clamp(20px, 4vw, 40px);
--mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
/* Sora throughout per brief 2026-05-02. Display + body share the same
* stack; weight differs by role. */
--display: 'Sora', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
--sans: 'Sora', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
}
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
font-family: var(--sans);
background: var(--bg);
color: var(--fg);
font-size: 16px;
line-height: 1.55;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-feature-settings: "ss01", "cv11";
}
::selection { background: rgb(86 180 233 / 0.35); } /* --info @ 35% */
/* Always-visible focus ring on every interactive element, keyboard-only
* by default. Pointer-driven focus is suppressed by :focus-visible. */
:focus { outline: none; }
:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 2px;
border-radius: 4px;
}
a {
color: var(--acc-info);
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 2px;
}
a:hover { color: var(--fg); text-decoration-thickness: 2px; }
/* ─── header / footer ─── */
.shell {
min-height: 100vh;
display: grid;
grid-template-rows: auto 1fr auto;
}
header.app {
border-bottom: 1px solid var(--line);
padding: 14px var(--pad-x);
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.brand {
display: flex;
align-items: center; /* vertically center the bottle with the wordmark */
gap: 12px;
min-width: 0;
}
.wordmark {
font-family: var(--display);
font-weight: 600;
font-size: 22px;
letter-spacing: -0.02em;
color: var(--fg);
line-height: 1;
}
.wordmark .dot {
color: var(--brand);
}
.brand-mark-link {
display: inline-flex;
align-items: center;
text-decoration: none;
}
.brand-mark-link:hover { background: transparent; }
.brand-mark {
width: 40px; height: 40px;
object-fit: contain;
flex-shrink: 0;
display: block;
transition: transform .18s ease-out;
}
.brand-mark-link:hover .brand-mark { transform: rotate(-6deg) scale(1.05); }
/* "(:|)" mark next to "indifferent ketchup" footer link. */
.ik-mark {
font-family: var(--mono);
font-weight: 600;
letter-spacing: -0.05em;
margin-left: 2px;
}
/* Footer links per brief: brand link in --brand, info link in --info,
* both underlined by default. */
.footer-link { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px; }
.footer-link-brand { color: var(--brand); }
.footer-link-brand:hover { color: var(--brand-hover); }
.footer-link-info { color: var(--info); }
.footer-link-info:hover { color: var(--fg); }
/* Tagline ("sorted. sort of.") in --fg-muted, italic per brief. */
.tagline {
font-family: var(--display);
font-style: italic;
font-size: 12px;
color: var(--fg-muted);
line-height: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.head-right {
display: flex;
align-items: center;
gap: 4px;
}
/* Header chrome links (github, docs). Per brief: --fg-muted in Sora 400,
* underline on hover only. */
.icon-btn {
appearance: none;
border: 1px solid transparent;
background: transparent;
color: var(--fg-muted);
height: 30px;
padding: 0 10px;
border-radius: var(--radius-sm);
font-family: var(--display);
font-weight: 400;
font-size: 13px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
text-decoration: none;
transition: color .12s, background .12s;
}
.icon-btn:hover {
color: var(--fg);
background: var(--bg-elevated);
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 3px;
}
.icon-btn svg { width: 14px; height: 14px; opacity: .85; }
/* Header CTA: distinct accent so the report-broken-mod button reads as an
* action, not a docs/github link. Hover ramps to filled brand color. */
.icon-btn.report {
color: var(--brand);
border: 1px solid var(--brand);
background: transparent;
cursor: pointer;
font-family: var(--mono);
}
.icon-btn.report:hover {
background: var(--brand);
color: var(--brand-ink);
border-color: var(--brand);
text-decoration: none;
}
/* ── Reported broken mods view ─────────────────────────────────────── */
.reports-panel {
display: block;
max-width: 1100px;
margin: 16px auto;
padding: 0 16px;
}
.reports-head {
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px 16px;
margin-bottom: 14px;
}
.reports-title {
font-family: var(--display);
font-size: 22px;
color: var(--fg);
margin-bottom: 4px;
}
.reports-blurb {
font-family: var(--mono);
font-size: 12px;
color: var(--fg-muted);
margin-bottom: 12px;
line-height: 1.5;
}
.reports-form {
display: flex;
gap: 8px;
align-items: stretch;
flex-wrap: wrap;
margin-bottom: 8px;
}
.reports-form .field {
flex: 1 1 220px;
min-height: 36px;
font-family: var(--mono);
font-size: 13px;
}
.reports-form select.field { flex: 0 0 auto; min-width: 240px; }
.reports-form .sort-btn { flex: 0 0 auto; min-width: 120px; }
.reports-error {
color: var(--error);
font-family: var(--mono);
font-size: 12px;
margin-bottom: 8px;
}
.reports-search { width: 100%; margin-top: 4px; }
.reports-meta {
font-family: var(--mono);
font-size: 11.5px;
color: var(--fg-muted);
margin-top: 8px;
}
.reports-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 6px;
}
.reports-empty {
font-family: var(--mono);
font-size: 13px;
color: var(--fg-muted);
padding: 24px 16px;
text-align: center;
border: 1px dashed var(--border);
border-radius: var(--radius);
}
/* Each row is a 6-cell grid: name | wsid | version | date | up | down.
* Wraps cleanly on narrow viewports — the votes column stays right-anchored. */
.report-row {
display: grid;
grid-template-columns: minmax(0, 1.6fr) 110px 130px 150px auto auto;
align-items: center;
gap: 10px;
padding: 8px 12px;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius);
font-family: var(--mono);
font-size: 13px;
}
.report-row:hover { border-color: var(--line-2); }
.report-name {
color: var(--fg);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.report-wsid { color: var(--fg-muted); font-size: 12px; }
.report-ver .ver-pill {
background: var(--bg-panel);
color: var(--fg-2);
border: 1px solid var(--border);
padding: 2px 8px;
border-radius: 3px;
font-size: 11.5px;
white-space: nowrap;
}
.report-date { color: var(--fg-muted); font-size: 12px; white-space: nowrap; }
.report-votes {
display: inline-flex;
gap: 6px;
align-items: center;
}
.vote-btn {
appearance: none;
border: 1px solid var(--line-2);
background: transparent;
color: var(--fg-2);
font-family: var(--mono);
font-size: 12px;
padding: 4px 10px;
border-radius: var(--radius);
cursor: pointer;
transition: color .12s, background .12s, border-color .12s;
min-width: 54px;
}
.vote-btn.up:hover:not([disabled]) { color: var(--success, var(--acc-green)); border-color: var(--success, var(--acc-green)); }
.vote-btn.down:hover:not([disabled]) { color: var(--error); border-color: var(--error); }
.vote-btn.up.voted { color: var(--success, var(--acc-green)); border-color: var(--success, var(--acc-green)); background: var(--success-bg, rgba(80,180,120,0.10)); }
.vote-btn.down.voted { color: var(--error); border-color: var(--error); background: rgba(220,80,80,0.10); }
.vote-btn[disabled]:not(.voted) { opacity: 0.45; cursor: not-allowed; }
@media (max-width: 720px) {
.report-row {
grid-template-columns: 1fr 1fr;
}
.report-name { grid-column: 1 / -1; }
.report-votes { justify-content: flex-end; }
}
footer.app {
border-top: 1px solid var(--line);
padding: 18px var(--pad-x);
display: flex;
flex-wrap: wrap;
gap: 8px 24px;
align-items: center;
justify-content: space-between;
color: var(--fg-2);
font-size: 12px;
font-family: var(--mono);
}
footer.app .left, footer.app .right {
display: flex; flex-wrap: wrap; gap: 4px 16px; align-items: center;
}
/* ─── main grid ─── */
main.app {
padding: clamp(20px, 3vw, 32px) var(--pad-x) clamp(32px, 5vw, 56px);
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 24px;
max-width: 1480px;
width: 100%;
margin: 0 auto;
}
@media (min-width: 1024px) {
main.app {
grid-template-columns: minmax(380px, 5fr) minmax(0, 7fr);
gap: 28px;
}
}
.col {
display: flex;
flex-direction: column;
gap: 16px;
min-width: 0;
}
/* ─── shared bits ─── */
.label-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 6px;
}
.label {
font-family: var(--mono);
font-size: 12px;
text-transform: lowercase;
letter-spacing: 0.02em;
color: var(--fg-2);
}
.label-meta {
font-family: var(--mono);
font-size: 11.5px;
color: var(--fg-2);
}
textarea.field, input.field {
width: 100%;
background: var(--bg-2);
color: var(--fg);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 12px 14px;
font-family: var(--mono);
font-size: 13px;
line-height: 1.55;
resize: vertical;
outline: none;
transition: border-color .12s, background .12s;
}
textarea.field:focus, input.field:focus {
border-color: var(--focus-ring);
background: var(--bg-elevated);
}
textarea.field:focus-visible, input.field:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 2px;
}
textarea.field::placeholder { color: var(--fg-2); }
.input-main { min-height: 220px; }
.input-rules { min-height: 110px; }
details.collapsible > summary {
list-style: none;
cursor: pointer;
font-family: var(--mono);
font-size: 12px;
font-weight: 600;
color: var(--fg);
user-select: none;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 5px 12px;
border: 1px solid var(--line-2);
background: var(--bg-1);
border-radius: var(--radius);
transition: color .12s, border-color .12s, background .12s;
}
details.collapsible > summary::-webkit-details-marker { display: none; }
details.collapsible > summary::before {
content: "▸";
color: var(--acc-blue);
font-size: 12px;
transition: transform .12s;
display: inline-block;
width: 9px;
}
details.collapsible[open] > summary::before { transform: rotate(90deg); }
details.collapsible > summary:hover {
color: var(--acc-blue);
border-color: var(--acc-blue);
background: var(--acc-blue-bg, rgba(70,140,220,0.10));
}
details.collapsible[open] > summary { margin-bottom: 8px; }
/* sort button */
/* Sort button = primary CTA. Solid brand fill, dark ink, glyph prefix.
* Hover brightens, active darkens, disabled goes flat with a "—" prefix. */
.sort-btn {
appearance: none;
width: 100%;
height: 42px;
border: 1px solid var(--brand);
background: var(--brand);
color: var(--brand-ink);
border-radius: var(--radius);
font-family: var(--display);
font-size: 14px;
font-weight: 700;
letter-spacing: 0.01em;
cursor: pointer;
transition: filter .12s, transform .04s, box-shadow .12s;
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
}
.sort-btn:hover {
background: var(--brand-hover);
border-color: var(--brand-hover);
box-shadow: var(--brand-shadow-card);
}
.sort-btn:active { filter: brightness(0.92); transform: translateY(1px); }
.sort-btn[disabled] {
cursor: not-allowed;
border-color: var(--border);
background: var(--bg-surface);
color: var(--fg-muted);
box-shadow: none;
filter: none;
}
.sort-btn .btn-glyph { font-size: 11px; line-height: 1; display: inline-block; min-width: 11px; text-align: center; }
/* Clear button — subordinate to sort. Outline-only, --border outline,
* --fg text. Hover lifts to --bg-elevated per brief. */
.clear-btn {
appearance: none;
height: 42px;
padding: 0 14px;
border: 1px solid var(--border);
background: transparent;
color: var(--fg);
border-radius: var(--radius);
font-family: var(--display);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.01em;
cursor: pointer;
display: inline-flex; align-items: center; gap: 8px;
transition: background .12s, color .12s, border-color .12s, transform .04s;
}
.clear-btn:hover { background: var(--bg-elevated); border-color: var(--fg-muted); }
.clear-btn:active { transform: translateY(1px); }
.sort-btn .spin {
width: 12px; height: 12px;
border: 1.5px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: sp 0.7s linear infinite;
}
@keyframes sp { to { transform: rotate(360deg); } }
/* status strip */
.status-strip {
display: flex; flex-wrap: wrap; align-items: center;
gap: 4px 12px;
font-family: var(--mono);
font-size: 11.5px;
color: var(--fg-2);
padding: 8px 0 0;
min-height: 24px;
}
.status-pill {
display: inline-flex; align-items: center; gap: 6px;
}
/* Glyph carries the state semantically (paired with a text label). The
* color reinforces the same meaning for sighted-but-not-CVD users. The
* old dot-led is preserved as a generic class for legacy callers; new
* pills use status-glyph. */
.status-glyph {
display: inline-block;
min-width: 12px;
text-align: center;
font-weight: 600;
line-height: 1;
}
.dot-led {
width: 6px; height: 6px; border-radius: 50%;
background: var(--fg-3);
}
/* State pill colors - explicit (color × glyph × label) per brief mapping.
* Each state gets a distinct hue from the Okabe-Ito palette so no two
* states collapse to the same color under deuteranopia/protanopia. */
.status-pill.idle { color: var(--fg-muted); }
.status-pill.done { color: var(--success); }
.status-pill.queued { color: var(--info); }
.status-pill.queued .status-glyph { animation: bl 1.2s ease-in-out infinite; }
.status-pill.working,
.status-pill.draining { color: var(--info); }
.status-pill.working .status-glyph,
.status-pill.draining .status-glyph { animation: bl 1.2s ease-in-out infinite; }
.status-pill.warning { color: var(--warning); }
.status-pill.failed { color: var(--error); }
.status-pill.muted { color: var(--fg-muted); }
/* Legacy classes (still referenced by some pills + callers) - alias to the
* new state colors so old code keeps working. */
.status-pill.cached { color: var(--success); }
.status-pill.parse { color: var(--info); }
.status-pill.parse .status-glyph { animation: bl 1.2s ease-in-out infinite; }
.status-pill.unknown { color: var(--error); }
.status-pill.nonmod { color: var(--fg-muted); }
.status-pill.expanding { color: var(--info); }
.status-pill.expanding .status-glyph { animation: bl 1.2s ease-in-out infinite; }
/* Dot-led background recolor (legacy callers) */
.status-pill.done .dot-led,
.status-pill.cached .dot-led { background: var(--success); }
.status-pill.queued .dot-led,
.status-pill.working .dot-led,
.status-pill.draining .dot-led,
.status-pill.parse .dot-led,
.status-pill.expanding .dot-led { background: var(--info); animation: bl 1.2s ease-in-out infinite; }
.status-pill.warning .dot-led { background: var(--warning); }
.status-pill.failed .dot-led,
.status-pill.unknown .dot-led { background: var(--error); }
.status-pill.muted .dot-led,
.status-pill.nonmod .dot-led { background: var(--fg-muted); }
.cancel-btn {
appearance: none;
border: 1px solid var(--line);
background: transparent;
color: var(--fg-2);
border-radius: var(--radius);
font-family: var(--mono);
font-size: 12px;
cursor: pointer;
transition: color .12s, border-color .12s, background .12s;
}
.cancel-btn:hover { color: var(--acc-red); border-color: var(--acc-red); background: var(--acc-red-bg); }
@keyframes bl { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
.progress-bar {
flex: 1 1 100%;
height: 2px;
background: var(--bg-2);
border-radius: 1px;
overflow: hidden;
margin-top: 4px;
}
.progress-bar > i {
display: block;
height: 100%;
background: var(--acc-blue);
transition: width .35s ease;
}
/* card / section panels - IB-style soft drop shadow under the card. */
.panel {
background: var(--bg-1);
border: 1px solid var(--line);
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--brand-shadow-card);
}
/* code blocks */
.code-block {
background: var(--bg-1);
border: 1px solid var(--line);
border-radius: var(--radius);
overflow: hidden;
}
.code-block .cb-head {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 12px;
background: var(--bg-2);
border-bottom: 1px solid var(--line);
font-family: var(--mono);
font-size: 12px;
color: var(--fg-2);
}
.code-block .cb-head .cb-key {
color: var(--fg-1);
font-weight: 500;
}
.code-block .cb-head .cb-meta {
color: var(--fg-2);
font-variant-numeric: tabular-nums;
}
.code-block pre {
margin: 0;
padding: 14px 16px;
font-family: var(--mono);
font-size: 13px;
line-height: 1.55;
color: var(--fg);
white-space: pre-wrap;
word-break: break-all;
overflow-x: auto;
min-height: 44px;
}
.code-block pre .ink-key { color: var(--acc-blue); }
.code-block pre .ink-sep { color: var(--fg-2); }
.code-block pre .ink-mut { color: var(--fg-2); }
.copy-btn {
appearance: none;
border: 1px solid var(--acc-blue);
background: var(--acc-blue-bg, rgba(70,140,220,0.10));
color: var(--acc-blue);
height: 26px;
padding: 0 10px;
border-radius: 5px;
font-family: var(--mono);
font-size: 12px;
font-weight: 600;
cursor: pointer;
display: inline-flex; align-items: center; gap: 6px;
transition: color .12s, background .12s, border-color .12s, transform .08s;
}
.copy-btn:hover { color: var(--brand-ink); background: var(--acc-blue); border-color: var(--acc-blue); }
.copy-btn:active { transform: translateY(1px); }
.copy-btn.copied { color: var(--acc-green); border-color: var(--acc-green); background: var(--acc-green-bg, rgba(80,180,120,0.12)); }
.copy-btn.copied:hover { color: var(--brand-ink); background: var(--acc-green); }
.copy-btn svg { width: 11px; height: 11px; }
/* multi-branch picker (Spec A) */
.branch-affordance {
appearance: none;
border: 1px solid var(--line);
background: transparent;
color: var(--acc-blue);
font-family: var(--mono);
font-size: 12px;
padding: 1px 6px;
border-radius: 3px;
cursor: pointer;
}
.branch-affordance:hover { color: var(--fg); border-color: var(--line-2); background: var(--bg-3); }
.branch-panel {
background: var(--bg-2);
border-top: 1px dashed var(--line);
border-bottom: 1px dashed var(--line);
padding: 0;
}
.branch-panel-inner {
padding: 8px 12px 10px;
font-family: var(--mono);
font-size: 12px;
}
.branch-panel-meta { color: var(--fg-2); margin-bottom: 6px; letter-spacing: 0.04em; }
.branch-row {
display: grid;
grid-template-columns: 18px 220px 1fr 70px 1fr 60px;
gap: 10px 10px;
align-items: center;
padding: 4px 0;
border-top: 1px solid var(--line);
cursor: pointer;
}
.branch-row:hover { background: var(--bg-panel); }
/* Picked row: --info per brief, not the brand green (status, not brand). */
.branch-row.is-checked .branch-modid { color: var(--info); font-weight: 600; }
.branch-input { accent-color: var(--info); }
.branch-modid { color: var(--fg); }
.branch-name { color: var(--fg-2); }
.branch-deps { color: var(--fg-2); font-size: 11.5px; }
.branch-pos { color: var(--fg-2); font-size: 11.5px; text-align: right; }
/* Spec C §4.6 D/G hint text: italic line under the mod_id row. */
.branch-hint {
grid-column: 2 / -1;
color: var(--acc-amber);
font-size: 11.5px;
font-style: italic;
margin-top: -2px;
padding-bottom: 2px;
}
/* diff panel (Spec follow-up: show what changed since last sort) */
.diff-toggle-row { display: flex; justify-content: flex-end; padding: 4px 0 2px; }
.diff-toggle {
appearance: none;
border: 1px solid var(--line-2);
background: transparent;
color: var(--fg-2);
font-family: var(--mono);
font-size: 12px;
font-weight: 600;
padding: 3px 10px;
border-radius: var(--radius);
cursor: pointer;
transition: color .12s, border-color .12s, background .12s;
}
.diff-toggle:hover:not(:disabled) {
color: var(--acc-blue);
border-color: var(--acc-blue);
background: var(--acc-blue-bg, rgba(70,140,220,0.10));
}
.diff-toggle.open {
color: var(--acc-blue);
border-color: var(--acc-blue);
background: var(--acc-blue-bg, rgba(70,140,220,0.10));
}
.diff-toggle:disabled { opacity: 0.4; cursor: not-allowed; }
.diff-panel {
border: 1px solid var(--line-2);
background: var(--bg-1);
border-radius: var(--radius);
padding: 10px 12px;
margin: 4px 0 10px;
font-family: var(--mono);
font-size: 12px;
}
.diff-head {
display: flex; align-items: center; gap: 10px;
padding-bottom: 8px; border-bottom: 1px solid var(--line);
margin-bottom: 8px;
}
.diff-title { color: var(--fg); font-weight: 600; }
.diff-summary { display: flex; gap: 6px; margin-left: auto; }
.diff-stat { padding: 1px 6px; border-radius: 3px; font-weight: 600; }
.diff-stat.add { color: var(--acc-green); background: var(--acc-green-bg, rgba(80,180,120,0.12)); }
.diff-stat.rm { color: var(--acc-red); background: var(--acc-red-bg, rgba(220,80,80,0.12)); }
.diff-stat.mv { color: var(--acc-amber); background: var(--acc-amber-bg, rgba(220,160,70,0.12)); }
.diff-close {
appearance: none;
border: 1px solid var(--line);
background: transparent;
color: var(--fg-2);
font-family: var(--mono);
font-size: 11.5px;
padding: 2px 8px;
border-radius: 4px;
cursor: pointer;
}
.diff-close:hover { color: var(--fg); border-color: var(--line-2); }
.diff-section { padding: 6px 0; }
.diff-label { color: var(--fg-2); font-size: 11.5px; text-transform: uppercase; letter-spacing: .04em; padding-bottom: 4px; }
.diff-row {
padding: 2px 0; display: flex; gap: 8px; align-items: baseline;
color: var(--fg);
}
.diff-row.diff-add { color: var(--acc-green); }
.diff-row.diff-rm { color: var(--acc-red); }
.diff-row.diff-mv { color: var(--acc-amber); }
.diff-arrow { width: 10px; text-align: center; }
.diff-pos { color: var(--fg-2); font-size: 11.5px; margin-left: auto; }
.diff-empty { color: var(--fg-2); font-style: italic; padding: 6px 0; }
.diff-more { color: var(--fg-2); font-size: 11.5px; padding: 4px 0; }
/* build toggle (B41 / B42) */
.build-toggle {
display: inline-flex; align-items: center; gap: 8px;
padding: 4px 0 8px;
font-family: var(--mono);
font-size: 11.5px;
color: var(--fg-2);
}
.build-toggle-label { letter-spacing: .04em; text-transform: lowercase; }
.build-toggle-btn {
appearance: none;
border: 1px solid var(--line);
background: var(--bg-1);
color: var(--fg-2);
height: 22px;
padding: 0 9px;
border-radius: 4px;
font-family: var(--mono);
font-size: 11.5px;
cursor: pointer;
transition: color .12s, background .12s, border-color .12s;
}
.build-toggle-btn:hover { color: var(--fg); border-color: var(--line-2); background: var(--bg-3); }
.build-toggle-btn.is-active {
color: var(--acc-green);
border-color: var(--acc-green);
background: var(--acc-green-bg);
}
.cg-build { color: var(--fg-2); }
/* Pre-paste build picker: sits at the top of the input column so the user
* sets the target build BEFORE pasting wsids. Backend uses pz_build to
* emit build-mismatch warnings on tagged mods that don't support it. */
.build-prepick {
display: flex; align-items: center; flex-wrap: wrap;
gap: 10px; padding-bottom: 6px;
}
.build-prepick .build-toggle { padding: 0; }
.build-prepick-hint {
font-family: var(--mono);
font-size: 11.5px;
color: var(--fg-2);
}
/* warnings */
.warn-section .warn-head {
display: flex; align-items: center; gap: 8px;
font-family: var(--mono);
font-size: 11.5px;
cursor: pointer;
user-select: none;
color: var(--fg-1);
padding: 10px 12px;
}
.warn-section .warn-head .badge {
display: inline-flex; align-items: center; justify-content: center;
min-width: 18px; height: 18px; padding: 0 6px;
border-radius: 9px;
font-size: 11.5px;
font-weight: 600;
background: var(--acc-amber-bg);
color: var(--acc-amber);
border: 1px solid var(--acc-amber);
}
.warn-section .warn-head .badge.red {
background: var(--acc-red-bg);
color: var(--acc-red);
border-color: var(--acc-red);
}
.warn-section .warn-head .chev {
margin-left: auto;
color: var(--fg-2);
font-size: 10px;
}
.warn-list {
list-style: none;
margin: 0;
padding: 0;
border-top: 1px solid var(--line);
}
.warn-list li {
display: grid;
grid-template-columns: 80px 1fr;
gap: 12px;
padding: 10px 12px;
font-family: var(--mono);
font-size: 12px;
border-bottom: 1px solid var(--line);
}
.warn-list li:last-child { border-bottom: 0; }
.warn-list .w-tag {
color: var(--fg-2);
font-size: 11.5px;
text-transform: uppercase;
letter-spacing: 0.04em;
padding-top: 1px;
}
.warn-list .w-tag.amber { color: var(--acc-amber); }
.warn-list .w-tag.red { color: var(--acc-red); }
.warn-list .w-content {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px 8px;
line-height: 1.55;
}
.warn-list .w-msg { color: var(--fg); }
.warn-list .w-msg em { color: var(--acc-blue); font-style: normal; }
.warn-action {
appearance: none;
border: 1px solid var(--acc-blue);
background: var(--acc-blue-bg, rgba(70,140,220,0.12));
color: var(--acc-blue);
font-family: var(--mono);
font-size: 12px;
font-weight: 600;
padding: 3px 10px;
border-radius: var(--radius);
cursor: pointer;
transition: color .12s, border-color .12s, background .12s, transform .08s;
}
.warn-action:hover {
background: var(--acc-blue);
color: var(--brand-ink);
}
.warn-action:active { transform: translateY(1px); }
.warn-action.expand {
border-color: var(--acc-amber);
background: var(--acc-amber-bg, rgba(220,160,70,0.12));
color: var(--acc-amber);
}
.warn-action.expand:hover {
background: var(--acc-amber);
color: var(--brand-ink);
border-color: var(--acc-amber);
}
/* External-link variant: search Steam Workshop when we can't auto-add. */
.warn-action.search {
border-color: var(--line-2);
background: transparent;
color: var(--fg-2);
text-decoration: none;
}
.warn-action.search:hover {
color: var(--acc-blue);
border-color: var(--acc-blue);
background: var(--acc-blue-bg, rgba(70,140,220,0.10));
}
/* Swap variant: distinct accent (success/green) since swapping is a
* deterministic resolution to a build-mismatch — strictly better than
* leaving the wrong-build wsid in the input. */
.warn-action.swap {
border-color: var(--success, var(--acc-green));
background: var(--success-bg, rgba(80,180,120,0.10));
color: var(--success, var(--acc-green));
}
.warn-action.swap:hover {
background: var(--success, var(--acc-green));
color: var(--brand-ink);
}
/* Remove variant: error-toned. Destructive in user-perception (drops a
* wsid from the input) but actually safer than leaving a wrong-build
* mod in. Hover flips to filled so accidental clicks are loud. */
.warn-action.remove {
border-color: var(--error);
background: transparent;
color: var(--error);
}
.warn-action.remove:hover {
background: var(--error);
color: var(--brand-ink);
border-color: var(--error);
}
/* Inline mod-name hyperlink inside warning messages. Inherits the message
* color so reading flow isn't disrupted; underline + brand color on hover
* signals it's clickable. */
.w-msg-link {
color: inherit;
text-decoration: none;
text-decoration-thickness: 1px;
text-underline-offset: 2px;
}
.w-msg-link:hover,
.w-msg-link:focus-visible {
color: var(--info);
text-decoration: underline;
}
.warn-branches {
flex-basis: 100%;
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 4px;
}
.warn-branch-btn {
appearance: none;
border: 1px solid var(--line);
background: transparent;
color: var(--fg-2);
font-family: var(--mono);
font-size: 12px;
padding: 2px 8px;
border-radius: var(--radius);
cursor: pointer;
transition: color .12s, border-color .12s, background .12s;
}
.warn-branch-btn:hover {
color: var(--acc-amber);
border-color: var(--acc-amber);
}
.warn-branch-btn.picked {
color: var(--acc-green);
border-color: var(--acc-green);
background: var(--acc-green-bg, rgba(80,180,120,0.08));
cursor: default;
}
/* mod details table */
.table-section .tbl-head {
display: flex; align-items: center; gap: 8px;
font-family: var(--mono);
font-size: 11.5px;
cursor: pointer;
user-select: none;
color: var(--fg-1);
padding: 10px 12px;
}
.table-section .tbl-head .count {
color: var(--fg-2);
font-variant-numeric: tabular-nums;
}
.table-section .tbl-head .chev {
margin-left: auto; color: var(--fg-2); font-size: 10px;
}
.mods-table {
width: 100%;
border-collapse: collapse;
font-family: var(--mono);
font-size: 12px;
border-top: 1px solid var(--line);
}
.mods-table th, .mods-table td {
padding: 9px 12px;
text-align: left;
border-bottom: 1px solid var(--line);
vertical-align: top;
}
.mods-table th {
background: var(--bg-2);
color: var(--fg-2);
font-weight: 500;
font-size: 11.5px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.mods-table tr:last-child td { border-bottom: 0; }
.mods-table td.idx { color: var(--fg-muted); width: 44px; font-variant-numeric: tabular-nums; padding-left: 6px; }
.row-state-glyph {
display: inline-block; min-width: 12px; margin-right: 6px;
text-align: center; font-weight: 700;
}
/* Per-row state: color + left-border pattern + glyph (in idx cell). */
.mod-row.mod-row-resolved td:first-child { border-left: 3px solid var(--success); }
.mod-row.mod-row-resolved .row-state-glyph { color: var(--success); }
.mod-row.mod-row-warning td:first-child { border-left: 3px dotted var(--warning); }
.mod-row.mod-row-warning .row-state-glyph { color: var(--warning); }
.mod-row.mod-row-unresolved td:first-child { border-left: 3px dashed var(--warning); }
.mod-row.mod-row-unresolved .row-state-glyph { color: var(--warning); }
.mod-row.mod-row-error td:first-child { border-left: 3px solid var(--error); }
.mod-row.mod-row-error .row-state-glyph { color: var(--error); }
.mods-table td .modid { color: var(--fg); }
.mods-table td .wsid { color: var(--fg-2); }
/* The wsid cell is a deep-link to the Steam Workshop page. Inherit the
* surrounding `.wsid` color so the row reads the same as before; only
* an underline + brand color on hover signals it's clickable. */
.wsid-link {
color: inherit;
text-decoration: none;
text-decoration-thickness: 1px;
text-underline-offset: 2px;
}
.wsid-link:hover,
.wsid-link:focus-visible {
color: var(--info);
text-decoration: underline;
}
.mods-table td .cat {
display: inline-block;
padding: 1px 6px;
border-radius: 3px;
background: var(--bg-3);
color: var(--fg-1);
font-size: 11.5px;
}
.mods-table td .cat.map { background: var(--info-bg); color: var(--info); }
.mods-table td .cat.lib { background: var(--warning-bg); color: var(--warning); }
/* Spec G-patch: patch pill - uses warning tone since patches load LAST and
* may need attention. Distinct from .cat.lib (also warning-toned) by the
* label text "patch" vs "lib" - color is reinforcement, not the carrier. */
.mods-table td .cat.patch { background: var(--bg-panel); color: var(--fg-muted); border: 1px solid var(--border); }
.mods-table td .deps {
color: var(--fg-2);
}
.mods-table td .pos {
color: var(--fg-2);
font-size: 11.5px;
}
.mods-table td .pos.first { color: var(--acc-green); }
.mods-table td .pos.last { color: var(--acc-amber); }
/* error banner */
.err-banner {
display: flex; align-items: flex-start; gap: 12px;
padding: 12px 14px;
background: var(--acc-red-bg);
border: 1px solid var(--acc-red);
border-radius: var(--radius);
font-family: var(--mono);
font-size: 12px;
color: var(--fg);
}
.err-banner .err-tag {
color: var(--acc-red);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 11.5px;
padding-top: 1px;
}
.err-banner .err-msg { flex: 1; line-height: 1.55; color: var(--fg-1); }
.err-banner .err-actions {
display: flex; gap: 6px; margin-top: 8px;
}
/* cold-cache notice - matches the StatusStrip 'cold' pill (warning tone). */
.cold-banner {
display: flex; align-items: flex-start; gap: 12px;
padding: 12px 14px;
background: var(--warning-bg);
border: 1px solid var(--warning);
border-radius: var(--radius);
font-family: var(--mono);
font-size: 12px;
}
.cold-banner .err-tag {
color: var(--acc-blue);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 11.5px;
padding-top: 1px;
}
.cold-banner .err-msg { flex: 1; color: var(--fg-1); line-height: 1.55; }
/* empty-state docs panel */
.docs-panel {
background: var(--bg-1);
border: 1px solid var(--line);
border-radius: var(--radius);
overflow: hidden;
}
.docs-panel .dp-head {
padding: 10px 12px;
border-bottom: 1px solid var(--line);
font-family: var(--mono);
font-size: 11.5px;
color: var(--fg-1);
display: flex; align-items: center; justify-content: space-between;
}
.docs-panel .dp-body {
padding: 12px 14px;
font-family: var(--mono);
font-size: 11.5px;
color: var(--fg-1);
line-height: 1.7;
}
.docs-panel .dp-body ol {
margin: 0; padding: 0 0 0 18px;
}
.docs-panel .dp-body li { margin-bottom: 4px; }
.docs-panel .dp-body code {
background: var(--bg-2);
color: var(--fg);
padding: 1px 5px;
border-radius: 3px;
font-size: 12px;
}
.docs-panel .dp-body .hint {
color: var(--fg-2);
font-size: 12px;
margin-top: 8px;
border-top: 1px dashed var(--line);
padding-top: 8px;
}
/* empty/idle right-column placeholder */
.right-empty {
border: 1px dashed var(--line-2);
border-radius: var(--radius);
padding: 28px 20px;
color: var(--fg-2);
font-family: var(--mono);
font-size: 12.5px;
text-align: left;
display: flex; flex-direction: column; gap: 10px;
line-height: 1.6;
}
.right-empty .big {
color: var(--fg-1);
font-size: 14px;
}
.right-empty .arrow {
display: inline-block; margin-right: 8px; color: var(--acc-green);
}
.right-empty .ex {
background: var(--bg-2);
border: 1px solid var(--line);
border-radius: 5px;
padding: 8px 10px;
color: var(--fg-1);
margin-top: 4px;
font-size: 11.5px;
}
.right-empty .ex .ink-mut { color: var(--fg-2); }
/* state label tweak — color + glyph + label per brief mapping. */
.state-tag {
display: inline-flex; align-items: center; gap: 6px;
font-family: var(--mono); font-size: 12px;
color: var(--fg-muted);
text-transform: lowercase;
}
.state-tag-glyph {
display: inline-block; min-width: 12px; text-align: center;
font-weight: 700; line-height: 1;
color: var(--fg-muted);
}
.state-tag.state-tag-idle .state-tag-glyph { color: var(--fg-muted); }
.state-tag.state-tag-working .state-tag-glyph,
.state-tag.state-tag-queued .state-tag-glyph,
.state-tag.state-tag-draining .state-tag-glyph { color: var(--info); }
.state-tag.state-tag-done .state-tag-glyph { color: var(--success); }
.state-tag.state-tag-warning .state-tag-glyph { color: var(--warning); }
.state-tag.state-tag-failed .state-tag-glyph { color: var(--error); }
/* compact success treatment (variant b) */
.compact-grid {
display: grid;
grid-template-columns: 110px 1fr 28px;
gap: 0;
background: var(--bg-1);
border: 1px solid var(--line);
border-radius: var(--radius);
overflow: hidden;
}
.compact-grid .cg-row {
display: contents;
}
.compact-grid .cg-row > * {
border-bottom: 1px solid var(--line);
padding: 11px 12px;
}
.compact-grid .cg-row:last-child > * { border-bottom: 0; }
.compact-grid .cg-key {
font-family: var(--mono);
font-size: 12px;
color: var(--fg-2);
background: var(--bg-2);
border-right: 1px solid var(--line);
text-transform: lowercase;
letter-spacing: 0.02em;
align-self: stretch;
}
.compact-grid .cg-val {
font-family: var(--mono);
font-size: 12.5px;
color: var(--fg);
word-break: break-all;
overflow-x: auto;
line-height: 1.5;
}
.compact-grid .cg-cp {
border-right: 0;
border-left: 1px solid var(--line);
background: var(--bg-1);
display: flex; align-items: center; justify-content: center;
padding: 0;
cursor: pointer;
color: var(--fg-2);
transition: color .12s, background .12s;
}
.compact-grid .cg-cp:hover { color: var(--fg); background: var(--bg-2); }
.compact-grid .cg-cp.copied { color: var(--acc-green); }
.compact-grid .cg-cp svg { width: 12px; height: 12px; }
/* numbered/labeled treatment (variant c) */
.numbered-block {
background: var(--bg-1);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 14px 16px 12px;
position: relative;
font-family: var(--mono);
}
.numbered-block .nb-head {
display: flex; align-items: baseline; gap: 10px;
margin-bottom: 6px;
}
.numbered-block .nb-head .nb-num {
font-size: 11.5px;
color: var(--fg-2);
font-variant-numeric: tabular-nums;
}
.numbered-block .nb-head .nb-key {
color: var(--acc-blue);
font-size: 12px;
font-weight: 500;
}
.numbered-block .nb-head .nb-meta {
color: var(--fg-2);
font-size: 11.5px;
margin-left: auto;
}
.numbered-block .nb-pre {
color: var(--fg);
font-size: 13px;
line-height: 1.55;
word-break: break-all;
margin: 0;
white-space: pre-wrap;
}
.numbered-block .nb-cp {
position: absolute;
top: 10px; right: 10px;
}
/* tiny svg icons */
.ico { width: 13px; height: 13px; flex-shrink: 0; }
/* worked-example pre-fill chip on input */
.worked-hint {
position: absolute;
inset: auto 0 0 0;
padding: 6px 12px;
font-family: var(--mono); font-size: 11.5px;
color: var(--fg-2);
pointer-events: none;
background: linear-gradient(to top, var(--bg-2), transparent);
border-radius: 0 0 var(--radius) var(--radius);
}
/* responsive */
@media (max-width: 600px) {
header.app { padding: 12px 16px; flex-wrap: wrap; }
.tagline { display: none; }
main.app { padding: 16px; gap: 18px; }
.compact-grid { grid-template-columns: 80px 1fr 28px; }
.warn-list li { grid-template-columns: 1fr; gap: 4px; }
}
</style>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
<script type="text/babel" src="tweaks-panel.jsx"></script>
<script type="text/babel" src="sortof-data.jsx"></script>
<script type="text/babel" src="sortof-app.jsx"></script>
</body>
</html>