diff --git a/.gitignore b/.gitignore
index 8b7d186..b3618a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -113,6 +113,9 @@ tmp/
temp/
.tmp/
+# Brainstorming visual companion sessions
+.superpowers/
+
# Chrome extension specific
*.crx
*.pem
diff --git a/.impeccable/critique/2026-10-21T05-01-02Z__frontend-devtools-html.md b/.impeccable/critique/2026-10-21T05-01-02Z__frontend-devtools-html.md
new file mode 100644
index 0000000..eb9dc7d
--- /dev/null
+++ b/.impeccable/critique/2026-10-21T05-01-02Z__frontend-devtools-html.md
@@ -0,0 +1,30 @@
+---
+target: DevTools panel
+total_score: 17
+max_score: 40
+na_heuristics:
+p0_count: 1
+p1_count: 3
+target_identity: "file:C:\\Users\\ishab\\projects\\apq-debugger\\frontend\\devtools.html"
+target_fingerprint: "sha256:ff29165fbb1567ffacc53a9ca6ae9fc5dbda938cfc5f24783f3d3ddc0c3cee4e"
+target_path: "C:\\Users\\ishab\\projects\\apq-debugger\\frontend\\devtools.html"
+timestamp: 2026-10-21T05-01-02Z
+slug: frontend-devtools-html
+---
+# Critique: APQ Debugger DevTools panel (frontend/devtools.html)
+Method: dual-agent. Score 17/40 (Poor).
+
+Heuristics: 1 Status 2 | 2 Match 2 | 3 Control 2 | 4 Consistency 1 | 5 Error prevention 2 | 6 Recognition 2 | 7 Efficiency 1 | 8 Aesthetic 2 | 9 Recovery 2 | 10 Help 1
+
+Specificity: Kora's editorial identity (Italiana, cream canvas, grain, tracked mono eyebrows) is distinctive but foreign to a DevTools tool; nothing derives from hashes/interception/resolution.
+
+Detector (rendered, light): low-contrast FULL badge 3.95 (dark 3.66), selected-row text 4.2 (dark 3.8), chips 4.35/4.49, primary hover 4.4; tiny-text detail URL 11px; flat-type-hierarchy; cream-palette (intentional); monotonous-spacing (weak). Static cramped-padding on modal = false positive.
+
+Priority issues:
+- [P0] Docked layouts break: 520x400 detail placeholder blocks clicks on list; 760x420 sidebar+metrics leave <2 rows. -> adapt
+- [P1] Wrong register; Italiana numerals misread (components.css:726), eyebrows, grain. -> typeset, quieter
+- [P1] Query buried (one-line pre, h-scroll) + variables render as $1 (js/ui/request-detail.js:144). -> layout
+- [P1] Settings popover has no surface; focus halo cancelled on tabs/rows by reset block components.css:70-95. -> harden
+- [P2] No live/intercepting state, no safety framing at Start, resolved vs unresolved indistinguishable. -> clarify, onboard
+
+Personas: Alex (no shortcuts, >10 tab stops), Sam (no focus on tabs/rows, noisy live region, toggle state unannounced), Priya (docked 400px: ~4 rows visible, no status/timing).
diff --git a/.impeccable/design.json b/.impeccable/design.json
new file mode 100644
index 0000000..3725836
--- /dev/null
+++ b/.impeccable/design.json
@@ -0,0 +1,1010 @@
+{
+ "schemaVersion": 2,
+ "generatedAt": "2026-10-21T05:37:41.642Z",
+ "title": "Design System: APQ Debugger",
+ "extensions": {
+ "colorMeta": {
+ "surface": {
+ "role": "neutral",
+ "displayName": "Surface",
+ "canonical": "light-dark(#ffffff, #202124)",
+ "light": "#ffffff",
+ "dark": "#202124",
+ "lightOklch": "oklch(100.0% 0.000 89.9)",
+ "darkOklch": "oklch(24.8% 0.006 271.2)",
+ "purpose": "Table, panes and detail body.",
+ "tonalRamp": [
+ "#0b0b0b",
+ "#242424",
+ "#404040",
+ "#5d5d5d",
+ "#808080",
+ "#a4a4a4",
+ "#cacaca",
+ "#eeeeee"
+ ]
+ },
+ "surface-chrome": {
+ "role": "neutral",
+ "displayName": "Chrome Grey",
+ "canonical": "light-dark(#f1f3f4, #292a2d)",
+ "light": "#f1f3f4",
+ "dark": "#292a2d",
+ "lightOklch": "oklch(96.3% 0.003 228.8)",
+ "darkOklch": "oklch(28.5% 0.006 271.2)",
+ "purpose": "Tab strip, toolbars, table head, group and code headers.",
+ "tonalRamp": [
+ "#0a0b0c",
+ "#232425",
+ "#3e4041",
+ "#5c5e5f",
+ "#7f8081",
+ "#a3a5a6",
+ "#c9cbcc",
+ "#edeff0"
+ ]
+ },
+ "surface-raised": {
+ "role": "neutral",
+ "displayName": "Raised",
+ "canonical": "light-dark(#ffffff, #303134)",
+ "light": "#ffffff",
+ "dark": "#303134",
+ "lightOklch": "oklch(100.0% 0.000 89.9)",
+ "darkOklch": "oklch(31.3% 0.006 271.2)",
+ "purpose": "Menus and dialogs.",
+ "tonalRamp": [
+ "#0b0b0b",
+ "#242424",
+ "#404040",
+ "#5d5d5d",
+ "#808080",
+ "#a4a4a4",
+ "#cacaca",
+ "#eeeeee"
+ ]
+ },
+ "surface-sunken": {
+ "role": "neutral",
+ "displayName": "Sunken",
+ "canonical": "light-dark(#f8f9fa, #1b1c1e)",
+ "light": "#f8f9fa",
+ "dark": "#1b1c1e",
+ "lightOklch": "oklch(98.2% 0.002 247.8)",
+ "darkOklch": "oklch(22.6% 0.004 264.5)",
+ "purpose": "Query, variables and SDL code bodies.",
+ "tonalRamp": [
+ "#0b0b0c",
+ "#232425",
+ "#3f4041",
+ "#5d5e5e",
+ "#7f8081",
+ "#a4a5a5",
+ "#cacacb",
+ "#edeeef"
+ ]
+ },
+ "surface-hover": {
+ "role": "neutral",
+ "displayName": "Hover Wash",
+ "canonical": "light-dark(#eef1f6, #2f3236)",
+ "light": "#eef1f6",
+ "dark": "#2f3236",
+ "lightOklch": "oklch(95.7% 0.007 260.7)",
+ "darkOklch": "oklch(31.6% 0.008 255.6)",
+ "purpose": "Hover on rows, tabs, buttons, menu options.",
+ "tonalRamp": [
+ "#090b0e",
+ "#222428",
+ "#3d4044",
+ "#5b5e62",
+ "#7e8085",
+ "#a2a5a9",
+ "#c8cbcf",
+ "#eceff4"
+ ]
+ },
+ "selection": {
+ "role": "primary",
+ "displayName": "Selection Blue",
+ "canonical": "light-dark(#d3e3fd, #0f3a6e)",
+ "light": "#d3e3fd",
+ "dark": "#0f3a6e",
+ "lightOklch": "oklch(91.2% 0.040 260.1)",
+ "darkOklch": "oklch(35.1% 0.101 255.7)",
+ "purpose": "Selected row / type / active pill while the list is focused.",
+ "tonalRamp": [
+ "#030b1c",
+ "#192437",
+ "#344055",
+ "#515e74",
+ "#738198",
+ "#96a6be",
+ "#bccce5",
+ "#e5efff"
+ ]
+ },
+ "selection-idle": {
+ "role": "neutral",
+ "displayName": "Selection Grey",
+ "canonical": "light-dark(#e8eaed, #3c4043)",
+ "light": "#e8eaed",
+ "dark": "#3c4043",
+ "lightOklch": "oklch(93.6% 0.005 258.3)",
+ "darkOklch": "oklch(36.9% 0.007 240.0)",
+ "purpose": "Selected row when its list is not focused.",
+ "tonalRamp": [
+ "#0a0b0d",
+ "#232426",
+ "#3e4042",
+ "#5c5e60",
+ "#7e8083",
+ "#a3a5a7",
+ "#c9cbcd",
+ "#edeff2"
+ ]
+ },
+ "input-surface": {
+ "role": "neutral",
+ "displayName": "Input Surface",
+ "canonical": "light-dark(#ffffff, #202124)",
+ "light": "#ffffff",
+ "dark": "#202124",
+ "lightOklch": "oklch(100.0% 0.000 89.9)",
+ "darkOklch": "oklch(24.8% 0.006 271.2)",
+ "purpose": "Text inputs and selects.",
+ "tonalRamp": [
+ "#0b0b0b",
+ "#242424",
+ "#404040",
+ "#5d5d5d",
+ "#808080",
+ "#a4a4a4",
+ "#cacaca",
+ "#eeeeee"
+ ]
+ },
+ "divider": {
+ "role": "neutral",
+ "displayName": "Divider",
+ "canonical": "light-dark(#dadce0, #3c4043)",
+ "light": "#dadce0",
+ "dark": "#3c4043",
+ "lightOklch": "oklch(89.4% 0.006 264.5)",
+ "darkOklch": "oklch(36.9% 0.007 240.0)",
+ "purpose": "Every 1px region split and column rule; resting input border.",
+ "tonalRamp": [
+ "#0a0b0e",
+ "#232427",
+ "#3e4043",
+ "#5c5e61",
+ "#7e8084",
+ "#a3a5a8",
+ "#c8cace",
+ "#eceef2"
+ ]
+ },
+ "input-border": {
+ "role": "neutral",
+ "displayName": "Input Border",
+ "canonical": "light-dark(#80868b, #80868b)",
+ "light": "#80868b",
+ "dark": "#80868b",
+ "lightOklch": "oklch(61.7% 0.010 242.9)",
+ "darkOklch": "oklch(61.7% 0.010 242.9)",
+ "purpose": "Input border on hover (3:1).",
+ "tonalRamp": [
+ "#080c0f",
+ "#202529",
+ "#3b4045",
+ "#595e63",
+ "#7b8186",
+ "#9fa5ab",
+ "#c5cbd1",
+ "#e9eff5"
+ ]
+ },
+ "raised-border": {
+ "role": "neutral",
+ "displayName": "Raised Border",
+ "canonical": "light-dark(#c4c7c5, #5f6368)",
+ "light": "#c4c7c5",
+ "dark": "#5f6368",
+ "lightOklch": "oklch(82.7% 0.004 157.2)",
+ "darkOklch": "oklch(49.8% 0.009 253.9)",
+ "purpose": "Menu and dialog edge; kbd border; scrollbar thumb.",
+ "tonalRamp": [
+ "#0a0c0a",
+ "#222523",
+ "#3e403f",
+ "#5c5e5d",
+ "#7e817f",
+ "#a2a5a3",
+ "#c8cbc9",
+ "#ecefed"
+ ]
+ },
+ "ink": {
+ "role": "neutral",
+ "displayName": "Ink",
+ "canonical": "light-dark(#1f1f1f, #e3e3e3)",
+ "light": "#1f1f1f",
+ "dark": "#e3e3e3",
+ "lightOklch": "oklch(23.9% 0.000 89.9)",
+ "darkOklch": "oklch(91.6% 0.000 89.9)",
+ "purpose": "Primary text.",
+ "tonalRamp": [
+ "#0b0b0b",
+ "#242424",
+ "#404040",
+ "#5d5d5d",
+ "#808080",
+ "#a4a4a4",
+ "#cacaca",
+ "#eeeeee"
+ ]
+ },
+ "ink-secondary": {
+ "role": "neutral",
+ "displayName": "Ink Secondary",
+ "canonical": "light-dark(#5f6368, #a8acb1)",
+ "light": "#5f6368",
+ "dark": "#a8acb1",
+ "lightOklch": "oklch(49.8% 0.009 253.9)",
+ "darkOklch": "oklch(74.3% 0.008 253.9)",
+ "purpose": "Labels, column heads, hashes, times.",
+ "tonalRamp": [
+ "#090b0f",
+ "#212429",
+ "#3c4045",
+ "#5a5e63",
+ "#7c8186",
+ "#a1a5aa",
+ "#c6cbd1",
+ "#eaeff5"
+ ]
+ },
+ "ink-disabled": {
+ "role": "neutral",
+ "displayName": "Ink Disabled",
+ "canonical": "light-dark(#9aa0a6, #6f7378)",
+ "light": "#9aa0a6",
+ "dark": "#6f7378",
+ "lightOklch": "oklch(70.3% 0.011 248.0)",
+ "darkOklch": "oklch(55.4% 0.009 253.9)",
+ "purpose": "Disabled controls.",
+ "tonalRamp": [
+ "#080c10",
+ "#202529",
+ "#3b4045",
+ "#595e64",
+ "#7b8187",
+ "#9fa5ab",
+ "#c5cbd2",
+ "#e9eff6"
+ ]
+ },
+ "ink-on-selection": {
+ "role": "neutral",
+ "displayName": "Ink on Selection",
+ "canonical": "light-dark(#041e49, #e3eefe)",
+ "light": "#041e49",
+ "dark": "#e3eefe",
+ "lightOklch": "oklch(24.6% 0.087 259.7)",
+ "darkOklch": "oklch(94.6% 0.025 257.6)",
+ "purpose": "Text on Selection Blue.",
+ "tonalRamp": [
+ "#00072a",
+ "#07224d",
+ "#233f6d",
+ "#405e8e",
+ "#6181b4",
+ "#84a6db",
+ "#adccfe",
+ "#e6effe"
+ ]
+ },
+ "devtools-blue": {
+ "role": "primary",
+ "displayName": "DevTools Blue",
+ "canonical": "light-dark(#0b57d0, #a8c7fa)",
+ "light": "#0b57d0",
+ "dark": "#a8c7fa",
+ "lightOklch": "oklch(49.5% 0.199 260.6)",
+ "darkOklch": "oklch(82.5% 0.080 260.3)",
+ "purpose": "Active tab, primary button, links, checkbox accent.",
+ "tonalRamp": [
+ "#00052e",
+ "#001e5a",
+ "#003790",
+ "#0452cb",
+ "#3179f4",
+ "#6ea4ff",
+ "#adccff",
+ "#e6efff"
+ ]
+ },
+ "devtools-blue-hover": {
+ "role": "primary",
+ "displayName": "DevTools Blue Hover",
+ "canonical": "light-dark(#0842a0, #c2d7fb)",
+ "light": "#0842a0",
+ "dark": "#c2d7fb",
+ "lightOklch": "oklch(40.9% 0.163 260.6)",
+ "darkOklch": "oklch(87.5% 0.055 261.2)",
+ "purpose": "Primary button hover.",
+ "tonalRamp": [
+ "#00052e",
+ "#001e5a",
+ "#003791",
+ "#2058b7",
+ "#437de0",
+ "#6fa4fe",
+ "#adccfe",
+ "#e6effe"
+ ]
+ },
+ "on-devtools-blue": {
+ "role": "primary",
+ "displayName": "On DevTools Blue",
+ "canonical": "light-dark(#ffffff, #062e6f)",
+ "light": "#ffffff",
+ "dark": "#062e6f",
+ "lightOklch": "oklch(100.0% 0.000 89.9)",
+ "darkOklch": "oklch(32.1% 0.121 260.2)",
+ "purpose": "Text on primary buttons.",
+ "tonalRamp": [
+ "#0b0b0b",
+ "#242424",
+ "#404040",
+ "#5d5d5d",
+ "#808080",
+ "#a4a4a4",
+ "#cacaca",
+ "#eeeeee"
+ ]
+ },
+ "focus": {
+ "role": "primary",
+ "displayName": "Focus Blue",
+ "canonical": "light-dark(#0b57d0, #a8c7fa)",
+ "light": "#0b57d0",
+ "dark": "#a8c7fa",
+ "lightOklch": "oklch(49.5% 0.199 260.6)",
+ "darkOklch": "oklch(82.5% 0.080 260.3)",
+ "purpose": "2px focus outline; resize handle active.",
+ "tonalRamp": [
+ "#00052e",
+ "#001e5a",
+ "#003790",
+ "#0452cb",
+ "#3179f4",
+ "#6ea4ff",
+ "#adccff",
+ "#e6efff"
+ ]
+ },
+ "record-red": {
+ "role": "secondary",
+ "displayName": "Record Red",
+ "canonical": "light-dark(#d93025, #f28b82)",
+ "light": "#d93025",
+ "dark": "#f28b82",
+ "lightOklch": "oklch(57.8% 0.206 29.0)",
+ "darkOklch": "oklch(74.5% 0.127 25.8)",
+ "purpose": "Recording circle, Intercepting status, live infobar dot.",
+ "tonalRamp": [
+ "#210000",
+ "#4c0000",
+ "#7d0000",
+ "#b20102",
+ "#e1392c",
+ "#ff7362",
+ "#ffb5aa",
+ "#ffe9e5"
+ ]
+ },
+ "resolved-green": {
+ "role": "tertiary",
+ "displayName": "Resolved Green",
+ "canonical": "light-dark(#137333, #81c995)",
+ "light": "#137333",
+ "dark": "#81c995",
+ "lightOklch": "oklch(48.8% 0.130 149.0)",
+ "darkOklch": "oklch(77.4% 0.104 152.2)",
+ "purpose": "Resolved state only.",
+ "tonalRamp": [
+ "#001002",
+ "#002d0d",
+ "#014e1e",
+ "#0e7131",
+ "#3e9553",
+ "#65bb77",
+ "#8be29b",
+ "#c8ffd0"
+ ]
+ },
+ "awaiting-amber": {
+ "role": "tertiary",
+ "displayName": "Awaiting Amber",
+ "canonical": "light-dark(#8a5300, #fdd663)",
+ "light": "#8a5300",
+ "dark": "#fdd663",
+ "lightOklch": "oklch(49.4% 0.109 66.9)",
+ "darkOklch": "oklch(88.8% 0.139 90.6)",
+ "purpose": "Awaiting retry state; no-patterns warning; warning status.",
+ "tonalRamp": [
+ "#160700",
+ "#371d00",
+ "#5c3601",
+ "#845002",
+ "#ab722f",
+ "#d29755",
+ "#fbbd7b",
+ "#feebd8"
+ ]
+ },
+ "error-red": {
+ "role": "tertiary",
+ "displayName": "Error Red",
+ "canonical": "light-dark(#b3261e, #f2b8b5)",
+ "light": "#b3261e",
+ "dark": "#f2b8b5",
+ "lightOklch": "oklch(50.1% 0.178 28.7)",
+ "darkOklch": "oklch(83.4% 0.068 22.0)",
+ "purpose": "Not in registry, errors, destructive confirm.",
+ "tonalRamp": [
+ "#210000",
+ "#4c0000",
+ "#7d0001",
+ "#ac1d17",
+ "#d64a3d",
+ "#ff7363",
+ "#ffb5aa",
+ "#ffe9e5"
+ ]
+ },
+ "error-wash": {
+ "role": "tertiary",
+ "displayName": "Error Wash",
+ "canonical": "light-dark(#fce8e6, #3c1b18)",
+ "light": "#fce8e6",
+ "dark": "#3c1b18",
+ "lightOklch": "oklch(94.6% 0.022 24.4)",
+ "darkOklch": "oklch(26.9% 0.053 26.5)",
+ "purpose": "Schema error band, error toast.",
+ "tonalRamp": [
+ "#130807",
+ "#2e201f",
+ "#4b3b3a",
+ "#6a5957",
+ "#8d7b7a",
+ "#b2a09e",
+ "#d9c5c3",
+ "#fde9e7"
+ ]
+ },
+ "live-wash": {
+ "role": "secondary",
+ "displayName": "Live Yellow",
+ "canonical": "light-dark(#fef7e0, #3a3013)",
+ "light": "#fef7e0",
+ "dark": "#3a3013",
+ "lightOklch": "oklch(97.6% 0.031 92.9)",
+ "darkOklch": "oklch(31.3% 0.047 90.5)",
+ "purpose": "Interception infobar and warning toast background.",
+ "tonalRamp": [
+ "#100b00",
+ "#292412",
+ "#45402d",
+ "#645d4a",
+ "#86806c",
+ "#aba58f",
+ "#d1cab4",
+ "#f5eed8"
+ ]
+ },
+ "live-border": {
+ "role": "secondary",
+ "displayName": "Live Yellow Border",
+ "canonical": "light-dark(#f2d68b, #6b5718)",
+ "light": "#f2d68b",
+ "dark": "#6b5718",
+ "lightOklch": "oklch(88.3% 0.099 89.8)",
+ "darkOklch": "oklch(46.5% 0.083 90.7)",
+ "purpose": "Infobar and warning toast edge.",
+ "tonalRamp": [
+ "#110a00",
+ "#2e2200",
+ "#4f3d01",
+ "#735a03",
+ "#977d32",
+ "#bda258",
+ "#e4c87d",
+ "#ffedbf"
+ ]
+ },
+ "live-ink": {
+ "role": "secondary",
+ "displayName": "Live Ink",
+ "canonical": "light-dark(#3c2c00, #fdecb7)",
+ "light": "#3c2c00",
+ "dark": "#fdecb7",
+ "lightOklch": "oklch(30.3% 0.062 87.0)",
+ "darkOklch": "oklch(94.4% 0.070 92.4)",
+ "purpose": "Text on Live Yellow.",
+ "tonalRamp": [
+ "#120a00",
+ "#2f2200",
+ "#4e3d14",
+ "#6d5b33",
+ "#907e55",
+ "#b5a378",
+ "#dcc99d",
+ "#ffedc4"
+ ]
+ },
+ "passive-wash": {
+ "role": "secondary",
+ "displayName": "Passive Blue Wash",
+ "canonical": "light-dark(#e8f0fe, #172b4d)",
+ "light": "#e8f0fe",
+ "dark": "#172b4d",
+ "lightOklch": "oklch(95.3% 0.021 261.8)",
+ "darkOklch": "oklch(29.2% 0.068 260.4)",
+ "purpose": "Passive-mode infobar background.",
+ "tonalRamp": [
+ "#070b14",
+ "#1e242e",
+ "#3a404b",
+ "#575e6a",
+ "#79818d",
+ "#9ea5b2",
+ "#c3cbd8",
+ "#e7effd"
+ ]
+ },
+ "passive-border": {
+ "role": "secondary",
+ "displayName": "Passive Border",
+ "canonical": "light-dark(#c2d7fb, #2c4a7a)",
+ "light": "#c2d7fb",
+ "dark": "#2c4a7a",
+ "lightOklch": "oklch(87.5% 0.055 261.2)",
+ "darkOklch": "oklch(41.0% 0.089 259.4)",
+ "purpose": "Passive infobar edge.",
+ "tonalRamp": [
+ "#010922",
+ "#14233e",
+ "#2f405d",
+ "#4c5e7d",
+ "#6e81a2",
+ "#92a5c8",
+ "#b7ccef",
+ "#e6effe"
+ ]
+ },
+ "passive-ink": {
+ "role": "secondary",
+ "displayName": "Passive Ink",
+ "canonical": "light-dark(#062e6f, #d3e3fd)",
+ "light": "#062e6f",
+ "dark": "#d3e3fd",
+ "lightOklch": "oklch(32.1% 0.121 260.2)",
+ "darkOklch": "oklch(91.2% 0.040 260.1)",
+ "purpose": "Text on Passive Blue Wash.",
+ "tonalRamp": [
+ "#00062c",
+ "#001e58",
+ "#153c7e",
+ "#335ca1",
+ "#5480c8",
+ "#78a5f0",
+ "#adccff",
+ "#e6effe"
+ ]
+ },
+ "update-flash": {
+ "role": "tertiary",
+ "displayName": "Update Flash",
+ "canonical": "light-dark(#fde293, #6b5718)",
+ "light": "#fde293",
+ "dark": "#6b5718",
+ "lightOklch": "oklch(91.8% 0.103 91.3)",
+ "darkOklch": "oklch(46.5% 0.083 90.7)",
+ "purpose": "Start colour of the resolve-in-place row flash.",
+ "tonalRamp": [
+ "#110a00",
+ "#2e2300",
+ "#4f3e00",
+ "#725b00",
+ "#977e2d",
+ "#bca254",
+ "#e3c97a",
+ "#ffeebc"
+ ]
+ },
+ "syntax-keyword": {
+ "role": "tertiary",
+ "displayName": "Syntax Keyword",
+ "canonical": "light-dark(#aa0d91, #e36eec)",
+ "light": "#aa0d91",
+ "dark": "#e36eec",
+ "lightOklch": "oklch(50.7% 0.217 336.8)",
+ "darkOklch": "oklch(72.1% 0.209 324.7)",
+ "purpose": "GraphQL keywords.",
+ "tonalRamp": [
+ "#1c0016",
+ "#430038",
+ "#6f005e",
+ "#9f0188",
+ "#ca3aae",
+ "#f463d5",
+ "#ffabe8",
+ "#ffe6f7"
+ ]
+ },
+ "syntax-type": {
+ "role": "tertiary",
+ "displayName": "Syntax Type",
+ "canonical": "light-dark(#0b57d0, #8ab4f8)",
+ "light": "#0b57d0",
+ "dark": "#8ab4f8",
+ "lightOklch": "oklch(49.5% 0.199 260.6)",
+ "darkOklch": "oklch(76.6% 0.108 259.8)",
+ "purpose": "Type names and built-ins.",
+ "tonalRamp": [
+ "#00052e",
+ "#001e5a",
+ "#003790",
+ "#0452cb",
+ "#3179f4",
+ "#6ea4ff",
+ "#adccff",
+ "#e6efff"
+ ]
+ },
+ "syntax-variable": {
+ "role": "tertiary",
+ "displayName": "Syntax Variable",
+ "canonical": "light-dark(#c5221f, #f28b82)",
+ "light": "#c5221f",
+ "dark": "#f28b82",
+ "lightOklch": "oklch(53.2% 0.198 27.9)",
+ "darkOklch": "oklch(74.5% 0.127 25.8)",
+ "purpose": "$variables and directives.",
+ "tonalRamp": [
+ "#210000",
+ "#4c0001",
+ "#7d0005",
+ "#b2010b",
+ "#dd3e35",
+ "#ff7365",
+ "#ffb5ab",
+ "#fee9e6"
+ ]
+ },
+ "syntax-string": {
+ "role": "tertiary",
+ "displayName": "Syntax String",
+ "canonical": "light-dark(#1a1aa6, #f29766)",
+ "light": "#1a1aa6",
+ "dark": "#f29766",
+ "lightOklch": "oklch(35.5% 0.209 269.0)",
+ "darkOklch": "oklch(76.0% 0.127 48.4)",
+ "purpose": "String literals.",
+ "tonalRamp": [
+ "#030036",
+ "#0e0075",
+ "#1c21ab",
+ "#3249d1",
+ "#4f70fb",
+ "#809fff",
+ "#b5c9ff",
+ "#e8eefe"
+ ]
+ },
+ "syntax-enum": {
+ "role": "tertiary",
+ "displayName": "Syntax Enum",
+ "canonical": "light-dark(#1c00cf, #b39ddb)",
+ "light": "#1c00cf",
+ "dark": "#b39ddb",
+ "lightOklch": "oklch(39.3% 0.263 267.9)",
+ "darkOklch": "oklch(73.7% 0.091 300.5)",
+ "purpose": "Enum values in SDL.",
+ "tonalRamp": [
+ "#020036",
+ "#0b0076",
+ "#1901bf",
+ "#2738ee",
+ "#4a70fe",
+ "#7e9fff",
+ "#b4c9ff",
+ "#e8eefe"
+ ]
+ },
+ "syntax-comment": {
+ "role": "tertiary",
+ "displayName": "Syntax Comment",
+ "canonical": "light-dark(#5f6368, #9aa0a6)",
+ "light": "#5f6368",
+ "dark": "#9aa0a6",
+ "lightOklch": "oklch(49.8% 0.009 253.9)",
+ "darkOklch": "oklch(70.3% 0.011 248.0)",
+ "purpose": "SDL descriptions/comments (italic).",
+ "tonalRamp": [
+ "#090b0f",
+ "#212429",
+ "#3c4045",
+ "#5a5e63",
+ "#7c8186",
+ "#a1a5aa",
+ "#c6cbd1",
+ "#eaeff5"
+ ]
+ }
+ },
+ "colorNotes": "Canonical values are light-dark() pairs in frontend/css/tokens.css; tonalRamp is synthesized in OKLCH from the light value. Dark mode flips color-scheme only (frontend/css/theme-dark.css). prefers-contrast: more strengthens divider and ink-secondary.",
+ "typographyMeta": {
+ "title": {
+ "displayName": "Title",
+ "purpose": "Detail operation name, empty-state and dialog titles. Largest UI text (13px)."
+ },
+ "body": {
+ "displayName": "Body",
+ "purpose": "Default for all interface text at DevTools 12px."
+ },
+ "label": {
+ "displayName": "Label",
+ "purpose": "Operation names, state words, column heads, primary/record buttons. Emphasis by weight 500."
+ },
+ "code": {
+ "displayName": "Code",
+ "purpose": "Query, variables and SDL bodies; wraps, never scrolls horizontally."
+ },
+ "code-small": {
+ "displayName": "Code Small",
+ "purpose": "Hashes, times, patterns, endpoint URLs, group headers, schema type names, inline code."
+ }
+ },
+ "density": [
+ {
+ "name": "comfortable",
+ "values": {
+ "fs": "12px",
+ "fs-sm": "11px",
+ "fs-title": "13px",
+ "toolbar-h": "30px",
+ "tab-h": "28px",
+ "control-h": "22px",
+ "row-h": "24px",
+ "pad": "12px"
+ },
+ "purpose": "DevTools default row height."
+ },
+ {
+ "name": "compact",
+ "values": {
+ "toolbar-h": "26px",
+ "tab-h": "26px",
+ "control-h": "20px",
+ "row-h": "20px",
+ "pad": "8px"
+ },
+ "purpose": "Dense tables; set via data-density=\"compact\" and applied automatically at max-height 460px."
+ }
+ ],
+ "shadows": [
+ {
+ "name": "shadow-menu",
+ "value": "0 1px 2px rgb(0 0 0 / 0.3), 0 2px 6px 2px rgb(0 0 0 / 0.15)",
+ "purpose": "Floating layers only: settings menu, toasts, dialog panel."
+ },
+ {
+ "name": "focus-outline",
+ "value": "outline: 2px solid var(--focus); outline-offset: 1px",
+ "purpose": "Focus indicator (an outline, not a shadow); inset -1px/-2px on rows, tabs, lists and code."
+ }
+ ],
+ "motion": [
+ {
+ "name": "ease-out",
+ "value": "cubic-bezier(0.2, 0, 0, 1)",
+ "purpose": "Standard easing for state transitions, the row flash and toast entry."
+ },
+ {
+ "name": "t-fast",
+ "value": "120ms",
+ "purpose": "Chevron and disclosure-triangle rotation."
+ },
+ {
+ "name": "t-med",
+ "value": "200ms",
+ "purpose": "Toast rise-in (6px)."
+ },
+ {
+ "name": "row-updated",
+ "value": "1.2s var(--ease-out), background from var(--flash)",
+ "purpose": "Signature resolve-in-place flash; reduced motion swaps it for a 1px resolved-green inset outline."
+ },
+ {
+ "name": "live-pulse",
+ "value": "1.2s ease-in-out x3, opacity to 0.35",
+ "purpose": "Record dot in the live infobar pulses three times, then rests."
+ },
+ {
+ "name": "skeleton",
+ "value": "1.4s ease-in-out infinite",
+ "purpose": "Schema loading shimmer."
+ }
+ ],
+ "breakpoints": [
+ {
+ "name": "narrow",
+ "value": "640px",
+ "purpose": "Below: detail pane overlays the list; Hash column, Patterns label and pane descriptions drop."
+ },
+ {
+ "name": "very-narrow",
+ "value": "420px",
+ "purpose": "Below: Time column, resolved count and toolbar separators drop; schema type list 140px."
+ },
+ {
+ "name": "short",
+ "value": "max-height: 460px",
+ "purpose": "Compact density applies automatically."
+ }
+ ]
+ },
+ "components": [
+ {
+ "name": "Toolbar Button",
+ "kind": "button",
+ "refersTo": "button-toolbar",
+ "description": "Default quiet control in toolbars; gains a divider outline on content surfaces.",
+ "html": "
",
+ "css": ".ds-toolbar{display:flex;gap:4px;padding:2px 6px;background:var(--bg-chrome);font:12px/1.4 var(--font-ui);color:var(--text)} .ds-btn{display:inline-flex;align-items:center;gap:4px;height:22px;padding:0 8px;border:1px solid transparent;border-radius:var(--r-md);background:none;color:var(--text);font:inherit;cursor:pointer} .ds-btn-outlined{border-color:var(--divider)} .ds-btn:hover{background:var(--bg-hover)} .ds-btn:focus-visible{outline:2px solid var(--focus);outline-offset:1px} .ds-btn:disabled{color:var(--text-disabled);background:none}"
+ },
+ {
+ "name": "Primary Button",
+ "kind": "button",
+ "refersTo": "button-primary",
+ "description": "One primary action per view (Load schema, Copy in the cURL dialog).",
+ "html": "",
+ "css": ".ds-btn-primary{height:22px;padding:0 8px;border:1px solid transparent;border-radius:var(--r-md);background:var(--primary);color:var(--on-primary);font:500 12px/1.4 var(--font-ui);cursor:pointer} .ds-btn-primary:hover{background:var(--primary-hover)} .ds-btn-primary:focus-visible{outline:2px solid var(--focus);outline-offset:1px}"
+ },
+ {
+ "name": "Record Button",
+ "kind": "button",
+ "refersTo": "button-toolbar",
+ "description": "Network-panel record circle: hollow when idle, filled red while intercepting.",
+ "html": " ",
+ "css": ".ds-rec{display:inline-flex;align-items:center;gap:4px;height:22px;padding:0 8px 0 6px;border:1px solid transparent;border-radius:var(--r-md);background:none;color:var(--text);font:500 12px/1.4 var(--font-ui);cursor:pointer} .ds-rec svg{fill:none;stroke:var(--text-2);stroke-width:2} .ds-rec.is-on{color:var(--record)} .ds-rec.is-on svg{fill:var(--record);stroke:var(--record)} .ds-rec:hover{background:var(--bg-hover)} .ds-rec:focus-visible{outline:2px solid var(--focus);outline-offset:1px}"
+ },
+ {
+ "name": "Type Filter Pills",
+ "kind": "chip",
+ "refersTo": "chip-filter",
+ "description": "DevTools Network resource-type pills: All / APQ / Full.",
+ "html": "",
+ "css": ".ds-pills{display:inline-flex;gap:2px;font:12px/1.4 var(--font-ui)} .ds-pill{height:20px;padding:0 8px;border:1px solid transparent;border-radius:var(--r-pill);background:none;color:var(--text);font:inherit;cursor:pointer} .ds-pill:hover{background:var(--bg-hover)} .ds-pill.is-active{background:var(--bg-selected);color:var(--text-on-selected)} .ds-pill:focus-visible{outline:2px solid var(--focus);outline-offset:1px}"
+ },
+ {
+ "name": "Toolbar Input",
+ "kind": "input",
+ "refersTo": "input-toolbar",
+ "description": "Filter and pattern fields; patterns use the monospace stack.",
+ "html": " ",
+ "css": ".ds-input{height:22px;width:180px;padding:0 6px;border:1px solid var(--divider);border-radius:var(--r-md);background:var(--bg-input);color:var(--text);font:12px/1.4 var(--font-ui)} .ds-input::placeholder{color:var(--text-2)} .ds-input:hover{border-color:var(--border-input)} .ds-input:focus-visible{outline:2px solid var(--focus);outline-offset:-1px;border-color:var(--focus)} .ds-mono{font-family:var(--font-mono);font-size:11px}"
+ },
+ {
+ "name": "Tab Strip",
+ "kind": "nav",
+ "refersTo": "tab",
+ "description": "Requests / Schema tabs with the blue underline; session status at the end.",
+ "html": "Intercepting
",
+ "css": ".ds-tabs{display:flex;align-items:stretch;min-height:28px;background:var(--bg-chrome);border-bottom:1px solid var(--divider);font:12px/1.4 var(--font-ui)} .ds-tab{position:relative;padding:0 12px;border:0;background:none;color:var(--text-2);font:inherit;cursor:pointer} .ds-tab:hover{color:var(--text);background:var(--bg-hover)} .ds-tab.is-on{color:var(--primary)} .ds-tab.is-on::after{content:\"\";position:absolute;left:0;right:0;bottom:-1px;height:2px;background:var(--primary)} .ds-tab:focus-visible{outline:2px solid var(--focus);outline-offset:-2px} .ds-status{margin-left:auto;padding:0 8px;display:inline-flex;align-items:center;gap:6px;color:var(--record);font-weight:600} .ds-status i{width:8px;height:8px;border-radius:50%;background:currentColor}"
+ },
+ {
+ "name": "Live Infobar",
+ "kind": "custom",
+ "refersTo": "infobar-live",
+ "description": "Yellow band shown only while matching traffic is being rewritten.",
+ "html": "Intercepting requests matching *graphql*
",
+ "css": ".ds-infobar{display:flex;align-items:flex-start;gap:8px;min-height:30px;padding:4px 12px;background:var(--live-bg);color:var(--live-text);border-bottom:1px solid var(--live-border);font:12px/1.4 var(--font-ui)} .ds-infobar svg{margin-top:4px;fill:var(--record);flex:none;animation:ds-pulse 1.2s ease-in-out 3} .ds-infobar p{flex:1;margin:0;padding-top:1px} .ds-infobar code{font:11px var(--font-mono);padding:0 2px;border-radius:var(--r-sm);background:color-mix(in srgb,currentColor 10%,transparent)} .ds-infobar button{height:22px;padding:0 8px;border:1px solid color-mix(in srgb,currentColor 35%,transparent);border-radius:var(--r-md);background:none;color:inherit;font:500 12px var(--font-ui);cursor:pointer} .ds-infobar button:hover{background:color-mix(in srgb,currentColor 10%,transparent)} .ds-infobar button:focus-visible{outline:2px solid var(--focus);outline-offset:1px} @keyframes ds-pulse{50%{opacity:.35}}"
+ },
+ {
+ "name": "Request Rows",
+ "kind": "custom",
+ "refersTo": "table-row",
+ "description": "Single-line rows with state as word + glyph; a resolving row flashes in place.",
+ "html": "NameStateHash
GetViewerResolvedecf4edb4
ProductListingAwaiting retry7a1f0c2e
AddToCartFull query
",
+ "css": ".ds-table{font:12px/1.4 var(--font-ui);color:var(--text);background:var(--bg);min-width:360px} .ds-row{display:grid;grid-template-columns:minmax(120px,1fr) 112px 76px;align-items:center;height:24px;border-bottom:1px solid color-mix(in srgb,var(--divider) 55%,transparent)} .ds-row>*{padding:0 8px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .ds-row>*+*{border-left:1px solid var(--divider);line-height:23px} .ds-head{background:var(--bg-chrome);color:var(--text-2);font-weight:500;border-bottom-color:var(--divider)} .ds-row:not(.ds-head):hover{background:var(--bg-hover)} .ds-row:focus-visible{outline:1px solid var(--focus);outline-offset:-1px} .ds-sel{background:var(--bg-selected);color:var(--text-on-selected)} .ds-op{font-weight:500} .ds-h{font:11px var(--font-mono);color:var(--text-2)} .ds-st{display:inline-flex;align-items:center;gap:4px;font-weight:500} .ds-st::before{content:\"\";width:7px;height:7px;flex:none} .ds-res{color:var(--resolved)} .ds-res::before{border-radius:50%;background:currentColor} .ds-pen{color:var(--pending)} .ds-pen::before{border-radius:50%;border:1.5px solid currentColor} .ds-full{color:inherit;font-weight:400} .ds-full::before{width:6px;height:6px;background:currentColor;border-radius:1px} .ds-flash{animation:ds-flash 1.2s var(--ease-out) infinite} @keyframes ds-flash{from{background:var(--flash)}} @media (prefers-reduced-motion:reduce){.ds-flash{animation:none;outline:1px solid var(--resolved);outline-offset:-1px}}"
+ },
+ {
+ "name": "Query Code Block",
+ "kind": "card",
+ "refersTo": "code-block",
+ "description": "Collapsible, wrapped, syntax-coloured query; the dominant element of the detail pane.",
+ "html": "Query
query GetViewer($id: ID!) {\n viewer(id: $id) { name }\n} ",
+ "css": ".ds-code{border:1px solid var(--divider);border-radius:var(--r-md);font:12px/1.4 var(--font-ui);color:var(--text)} .ds-code summary{display:flex;align-items:center;gap:6px;min-height:26px;padding:2px 2px 2px 8px;background:var(--bg-chrome);border-bottom:1px solid var(--divider);border-radius:var(--r-md) var(--r-md) 0 0;list-style:none;cursor:pointer;font-weight:600} .ds-code summary::-webkit-details-marker{display:none} .ds-code summary::before{content:\"\";border-style:solid;border-width:4px 0 4px 6px;border-color:transparent transparent transparent var(--text-2);transform:rotate(90deg);transition:transform var(--t-fast) var(--ease-out)} .ds-code summary span{flex:1} .ds-code summary:focus-visible{outline:2px solid var(--focus);outline-offset:-2px} .ds-code summary button{height:20px;padding:0 8px;border:1px solid transparent;border-radius:var(--r-md);background:none;color:var(--text);font:400 12px var(--font-ui);cursor:pointer} .ds-code summary button:hover{background:var(--bg-hover)} .ds-code pre{margin:0;padding:8px 12px;background:var(--bg-sunken);font:12px/1.5 var(--font-mono);white-space:pre-wrap;overflow-wrap:anywhere;border-radius:0 0 var(--r-md) var(--r-md)} .ds-code .k{color:var(--syn-keyword)} .ds-code .t{color:var(--syn-type)} .ds-code .v{color:var(--syn-variable)}"
+ },
+ {
+ "name": "Settings Menu",
+ "kind": "custom",
+ "refersTo": "menu",
+ "description": "Floating raised menu: the only in-panel surface (with toasts and dialogs) that carries a shadow.",
+ "html": "",
+ "css": ".ds-menu{width:240px;padding:6px 0;background:var(--bg-raised);border:1px solid var(--border-raised);border-radius:var(--r-lg);box-shadow:var(--shadow-menu);font:12px/1.4 var(--font-ui);color:var(--text)} .ds-menu fieldset{margin:0;padding:4px 0;border:0} .ds-menu legend{float:left;width:100%;padding:4px 12px;color:var(--text-2);font-weight:500} .ds-menu label{clear:both;display:flex;align-items:center;gap:8px;height:26px;padding:0 12px;cursor:pointer} .ds-menu label:hover{background:var(--bg-hover)} .ds-menu input{width:14px;height:14px;margin:0;accent-color:var(--primary)} .ds-menu input:focus-visible{outline:2px solid var(--focus);outline-offset:1px}"
+ }
+ ],
+ "narrative": {
+ "northStar": "The First-Party Panel",
+ "overview": "APQ Debugger is built to be mistaken for a panel Chrome shipped itself. It sits beside Network and Elements, so it takes their system wholesale: 12px system UI type, monospace for anything machine-made (hashes, queries, URLs, timestamps), white and grey-100 surfaces (dark: #202124 / #292a2d) split by 1px dividers, Google blue for selection, focus and the one primary action, record red, a yellow infobar while traffic is being rewritten, and green reserved for a resolved hash. Familiarity is the design goal; personality is spent only on state changes.\n\nThe structure is the DevTools canon: a tab strip (Requests, Schema, settings gear and session status at the right), a per-view toolbar, an optional collapsible patterns pane, an infobar when interception or passive mode is on, then a dense single-line data table with a detail pane that appears only after selection. In the detail pane the recovered query is the largest, most readable thing on screen, wrapped and syntax-coloured in DevTools Sources colours. The system rejects the editorial card-and-metric dashboard and the permanent settings sidebar that preceded it.\n\nDensity is a working constraint, not a preference: the panel is often docked about 400px tall. Comfortable matches DevTools' default row height (24px), Compact its dense tables (20px), and Compact switches on by itself when the panel is 460px tall or less. The one signature moment is a pending APQ row resolving in place, its state flipping to Resolved under the DevTools \"updated\" flash.",
+ "keyCharacteristics": [
+ "System UI 12px for interface, ui-monospace for code, hashes, URLs and times; no bundled or network fonts.",
+ "Flat in-flow surfaces separated by 1px dividers and tonal steps (surface vs chrome vs sunken).",
+ "Blue means selection, focus or primary; red means recording; yellow means traffic is being rewritten; green means resolved.",
+ "Every request state is a word plus a glyph shape, never colour alone.",
+ "Single-line 24px (20px compact) table rows with column dividers.",
+ "Every colour defined once with light-dark(); dark mode only flips color-scheme."
+ ],
+ "rules": [
+ {
+ "name": "The One Meaning Rule",
+ "body": "Each signal colour has exactly one job: blue is selection, focus and the primary action; red is recording; yellow is live rewriting or a warning; green is a resolved hash. A colour never borrows another's meaning.",
+ "section": "colors"
+ },
+ {
+ "name": "The Yellow Means Rewriting Rule",
+ "body": "The yellow infobar is visible if and only if the debugger is attached and rewriting matching traffic; it names the patterns in monospace and carries Stop. Passive mode gets the blue infobar instead, because nothing is modified.",
+ "section": "colors"
+ },
+ {
+ "name": "The Define-Once Rule",
+ "body": "Every colour is declared once in tokens.css as `light-dark(light, dark)`. Dark mode is a `color-scheme` flip in theme-dark.css (explicit `data-theme` wins; otherwise the system preference). No component redeclares a colour per theme.",
+ "section": "colors"
+ },
+ {
+ "name": "The Machine-Text Rule",
+ "body": "Anything the machine produced (hash, URL, pattern, timestamp, query, type name) is set in the monospace stack; anything a person reads as interface is in system-ui. Numbers that update in place use tabular numerals.",
+ "section": "typography"
+ },
+ {
+ "name": "The Flat Ramp Rule",
+ "body": "The ramp spans 11px to 13px. Hierarchy comes from weight (400/500/600), colour (ink vs ink-secondary) and position, never from display sizes. Sentence case throughout; no uppercase tracking, no eyebrows.",
+ "section": "typography"
+ },
+ {
+ "name": "The Docked-First Rule",
+ "body": "Design for the panel docked about 400px tall: no chrome may cost more than one toolbar row plus the optional infobar before data rows begin.",
+ "section": "layout"
+ },
+ {
+ "name": "The Overlays-Only Rule",
+ "body": "A shadow means \"floating above the panel\". Rows, panes, toolbars, code blocks and infobars never carry one.",
+ "section": "elevation"
+ }
+ ],
+ "dos": [
+ "Do take every colour from tokens.css and add new ones there as `light-dark(light, dark)` pairs with text at 4.5:1 and meaningful indicators at 3:1 in both themes.",
+ "Do separate regions with 1px dividers and the surface / chrome / sunken tonal steps.",
+ "Do express every request state as a word plus a glyph shape, using the vocabulary in `request-state.js`.",
+ "Do set hashes, URLs, patterns, times and code in the monospace stack, and counts in tabular numerals.",
+ "Do keep controls at the density heights (22/20px controls, 24/20px rows) so compact and the short-dock auto-compact keep working.",
+ "Do keep the query block wrapped and dominant in the detail pane; the recovered query is the largest readable thing on screen.",
+ "Do give every motion a reduced-motion fallback that keeps the meaning (the resolve flash becomes a green outline).",
+ "Do support forced colours: selection maps to Highlight/HighlightText and floating layers get a CanvasText border."
+ ],
+ "donts": [
+ "Don't add shadows to in-flow surfaces; the overlay shadow is for menus, toasts and dialogs only.",
+ "Don't use green for anything except a resolved hash, or yellow except live interception and warnings.",
+ "Don't signal state with colour alone.",
+ "Don't load fonts, icons or scripts from the network, or ship a display face; the platform system-ui and ui-monospace stacks are the type system.",
+ "Don't reintroduce the editorial card-and-metric dashboard, a permanent settings sidebar, uppercase tracked eyebrows or display numerals.",
+ "Don't redeclare colours for dark mode; flip `color-scheme` only.",
+ "Don't let the query scroll horizontally; it wraps."
+ ]
+ }
+}
diff --git a/.impeccable/review/dark-04-detail.png b/.impeccable/review/dark-04-detail.png
new file mode 100644
index 0000000..0de0f51
Binary files /dev/null and b/.impeccable/review/dark-04-detail.png differ
diff --git a/.impeccable/review/dark-05-detail-pending.png b/.impeccable/review/dark-05-detail-pending.png
new file mode 100644
index 0000000..1ee8ce9
Binary files /dev/null and b/.impeccable/review/dark-05-detail-pending.png differ
diff --git a/.impeccable/review/dark-07-schema-empty.png b/.impeccable/review/dark-07-schema-empty.png
new file mode 100644
index 0000000..d9703f6
Binary files /dev/null and b/.impeccable/review/dark-07-schema-empty.png differ
diff --git a/.impeccable/review/desktop.png b/.impeccable/review/desktop.png
new file mode 100644
index 0000000..bf107e1
Binary files /dev/null and b/.impeccable/review/desktop.png differ
diff --git a/.impeccable/review/light-01-first-run.png b/.impeccable/review/light-01-first-run.png
new file mode 100644
index 0000000..dcaa82c
Binary files /dev/null and b/.impeccable/review/light-01-first-run.png differ
diff --git a/.impeccable/review/light-03-resolving-flash.png b/.impeccable/review/light-03-resolving-flash.png
new file mode 100644
index 0000000..8c4821e
Binary files /dev/null and b/.impeccable/review/light-03-resolving-flash.png differ
diff --git a/.impeccable/review/light-06-settings.png b/.impeccable/review/light-06-settings.png
new file mode 100644
index 0000000..639b741
Binary files /dev/null and b/.impeccable/review/light-06-settings.png differ
diff --git a/.impeccable/review/light-08-docked-900x400.png b/.impeccable/review/light-08-docked-900x400.png
new file mode 100644
index 0000000..3d998ae
Binary files /dev/null and b/.impeccable/review/light-08-docked-900x400.png differ
diff --git a/.impeccable/review/light-09-narrow-520-list.png b/.impeccable/review/light-09-narrow-520-list.png
new file mode 100644
index 0000000..8f3c144
Binary files /dev/null and b/.impeccable/review/light-09-narrow-520-list.png differ
diff --git a/.impeccable/review/light-10-narrow-520-detail.png b/.impeccable/review/light-10-narrow-520-detail.png
new file mode 100644
index 0000000..ed02bf8
Binary files /dev/null and b/.impeccable/review/light-10-narrow-520-detail.png differ
diff --git a/.impeccable/review/light-11-focus-tab.png b/.impeccable/review/light-11-focus-tab.png
new file mode 100644
index 0000000..2164f62
Binary files /dev/null and b/.impeccable/review/light-11-focus-tab.png differ
diff --git a/.impeccable/review/light-12-focus-row.png b/.impeccable/review/light-12-focus-row.png
new file mode 100644
index 0000000..805b87c
Binary files /dev/null and b/.impeccable/review/light-12-focus-row.png differ
diff --git a/.impeccable/review/mobile.png b/.impeccable/review/mobile.png
new file mode 100644
index 0000000..928bfef
Binary files /dev/null and b/.impeccable/review/mobile.png differ
diff --git a/.impeccable/surfaces/frontend-devtools-html.md b/.impeccable/surfaces/frontend-devtools-html.md
new file mode 100644
index 0000000..6915def
--- /dev/null
+++ b/.impeccable/surfaces/frontend-devtools-html.md
@@ -0,0 +1,29 @@
+---
+version: 1
+slug: "frontend-devtools-html"
+primary_target: "frontend/devtools.html"
+related_targets: []
+---
+
+# Surface brief: DevTools panel
+
+Scope: the whole APQ Debugger DevTools panel (frontend/devtools.html, frontend/css/*, js/ui/*). Visitor mode: Operate.
+
+Audience/job: GraphQL developers mid-investigation in Chrome DevTools, often docked ~400px tall beside Network. Task: start interception on matching URLs, watch operations arrive, read and copy the recovered query/variables, optionally browse the schema. Constraints: MV3, Chrome 123+, nothing loaded from the network, keyboard and screen-reader operable, light and dark.
+
+User decisions (2026-10-21): direction pinned to DevTools-native (the category standard played straight; sits alongside Chrome DevTools Network/Elements panels at their craft level); full redesign incl. structure; bugs folded in; live bar plus first-run note for Start (no modal).
+
+## Direction contract
+
+THESIS: A panel that reads as a first-party DevTools tool: Network-style toolbar, single-line data rows, a detail pane where the recovered query is the largest, most readable thing. Refuses the editorial card-and-metric dashboard with a permanent settings sidebar.
+
+OWN-WORLD: Chrome DevTools system: system-ui 12px and ui-monospace for code/hashes; white/grey-100 surfaces (dark #202124/#292a2d) split by 1px dividers; Google blue for selection, focus and primary; record red; yellow infobar for live interception; green only for resolved; DevTools Sources syntax colours.
+
+STORY: The user sees at once whether traffic is being rewritten and on which patterns, watches hashes arrive and resolve, opens one and copies the query.
+
+FIRST VIEWPORT: Row 1 tab strip (Requests, Schema; settings gear right). Row 2 toolbar: record/stop, clear, export, filter, All/APQ/Full, endpoint select, Patterns toggle with summary, count "5 · 4 resolved". Optional collapsible patterns pane under it. Yellow live bar when intercepting. Table: Name, State, Hash, Time. Detail pane (right; full overlay when narrow) only after selection: name + state line, meta grid, wrapped highlighted query, variables.
+
+FORM: DevTools-native canon (user-pinned standing exit, position: canon). Signature move: a pending APQ row resolves in place when its retry lands (state flips to Resolved with the DevTools update flash). Roll skipped: the user pinned the direction, which beats the roll.
+seed: none
+
+FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9df4042..427c087 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,47 @@ All notable changes to the APQ Debugger extension will be documented in this fil
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [Unreleased]
+
+## [4.0.0] - 2026-10-23
+
+### Added
+- Observed endpoints in the patterns pane: GraphQL endpoints seen in live
+ traffic, with request counts and one-click "Add as pattern"
+- Request and resolved-hash counts in the toolbar
+- Endpoint filter and automatic endpoint group headers in the request list
+- Row states with a word and a shape for each: Resolved, Awaiting retry,
+ Not in registry, APQ retry, Full query; a hash row resolves in place when
+ its full-query retry arrives
+- Infobar while traffic is being rewritten, and in passive mode
+- Toast notifications (replacing the status banner), confirm dialogs before
+ clearing history or the hash registry, and skeleton loading for the schema
+ explorer
+- Comfortable and compact row density (compact applies automatically in short
+ DevTools docks)
+- New read-only `getObservedEndpoints` service-worker message
+
+### Changed
+- **Full UI redesign as a DevTools-native panel**, matching Network and
+ Elements: tab strip, per-view toolbar, collapsible patterns pane (open by
+ default until a pattern is saved), request table with a detail split that
+ overlays the list in narrow docks
+- System UI font only; no bundled or network fonts
+- Light and dark colours defined once via CSS `light-dark()` (requires
+ Chrome 123+, declared via `minimum_chrome_version`)
+- One-word status vocabulary: Off, Starting, Intercepting, Passive, Stopping,
+ Error
+- Schema SDL highlighting rewritten as a single-pass lexer: fields, arguments,
+ default values, directive arguments, interfaces, unions and directive
+ locations are now coloured correctly, in DevTools Sources colours
+- CSS build supports ordered multi-file concatenation with a size budget;
+ panel bundle budget raised to 55 KB
+
+### Fixed
+- SDL comments containing a string no longer render stray placeholder
+ characters in the schema viewer
+- All-caps type names (such as `URL`) are no longer coloured as enum values
+
## [3.0.0] - 2026-06-13
### Added
@@ -102,6 +143,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
| Version | Commit | Date |
|---------|--------|------|
+| 4.0.0 | — | 2026-10-23 |
| 3.0.0 | — | 2026-06-13 |
| 2.0.0 | — | 2026-02-07 |
| 1.1.0 | `5b035ba` | 2026-01-30 |
diff --git a/CLAUDE.md b/CLAUDE.md
index 826a28f..f97dcc6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -18,6 +18,8 @@ npx jest -t "should evict the oldest hash" # Single test by name
npm run test:e2e # Playwright e2e (launches real Chrome with the built extension)
npm run lint # ESLint
npm run format:check # Prettier check (see Windows note below)
+npm run promo:screens # Regenerate store screenshots + promo tiles (after npm run build)
+npm run promo:video # Re-record the demo video per store/promo/video/demo-script.md
```
Releases: `npm version patch|minor|major` (syncs manifest.json via `scripts/sync-version.js`), then `git push --follow-tags` — the release workflow builds, tests, and publishes the zip.
@@ -32,6 +34,8 @@ Two independently bundled contexts that communicate only via `chrome.runtime.sen
- **Service worker** (`js/sw/`, entry `index.js` → built as `service-worker.min.js`): owns all Chrome Debugger interaction. `debugger-manager.js` handles attach/detach lifecycle, `interceptor.js` handles `Fetch.requestPaused` and hash contamination, `messaging.js` routes incoming messages, `chrome-api.js` wraps callback APIs in promises.
- **DevTools panel** (`js/ui/`, entry `index.js` → built as `devtools.min.js` loaded by `frontend/devtools.html`): one module per UI concern, all sharing the mutable `state` object in `js/ui/state.js`. The panel identifies its tab via `chrome.devtools.inspectedWindow.tabId` (reliable even when undocked).
+- **UI design layer** (`frontend/css/`, concatenated in order into `devtools.min.css`): the panel is **DevTools-native** — it should read as a first-party Chrome DevTools panel next to Network/Elements (see `PRODUCT.md`; the earlier Kora styling was retired in Oct 2026). `tokens.css` (every color defined once via `light-dark()`; hence `minimum_chrome_version: 123`), `theme-dark.css` (the only dark-mode gates — flip `color-scheme`, never redeclare colors), `density.css` (row/control heights behind `data-density`; compact also applies automatically below 460px tall), `components.css` (all component styling). Rules that must not regress: system UI font at 12px and `ui-monospace` only for code, hashes and URLs (no bundled or network fonts); 1px dividers separate regions, and only floating layers (menus, dialogs, toasts) get a shadow; colour carries state only — blue for selection/focus/primary, record red, yellow infobar while traffic is rewritten, green only for a resolved hash — and state is never colour alone (each state has a word and a shape); never cancel `:focus-visible`; sentence-case copy and one-word status vocabulary (Off, Starting, Intercepting, Passive, Stopping, Error).
+- **Panel structure:** tab strip (Requests / Schema, status, settings) → per-view toolbar (record, clear, export, patterns toggle, filter, type pills, endpoint select, counts) → collapsible patterns pane (opens automatically when no pattern is saved) → infobar (`session.js`: live interception or passive mode) → table + detail split. The detail pane is hidden until a row is selected and overlays the list below 640px wide (`layout.js`). `request-state.js` owns the row vocabulary (Resolved / Awaiting retry / Not in registry / APQ retry / Full query); `request-list.js` resolves a hash row in place when its full-query retry with the same hash arrives.
- **`js/shared/`**: constants and logger used by both bundles. The GraphQL introspection query is a string literal here specifically so the `graphql` npm package stays out of the service worker bundle.
Key cross-cutting behaviors:
@@ -40,7 +44,7 @@ Key cross-cutting behaviors:
- **Hash registry / passive mode** (`js/sw/hash-registry.js`): active mode contaminates hashes and records hash→query mappings from the retries; passive mode resolves hashes from the registry without touching traffic. Capped at `HASH_REGISTRY_MAX_ENTRIES` (LRU eviction; insertion order is recency — re-registering deletes then re-sets the key).
- **Messaging convention:** SW→panel messages are broadcast with a `tabId` field and filtered on the receiving side; panel→SW messages are validated in `messaging.js` (`validateMessage`) before routing. Every `sendMessage` callback checks `chrome.runtime.lastError` because the panel is frequently not open.
- **Schema loading** (`js/sw/schema-loader.js`): runs in the SW (which has `` host permission for cross-origin fetch). Endpoints observed by the interceptor (`endpoint-tracker.js`) are known GraphQL endpoints, so they are introspected directly, replaying the request headers captured from live traffic (production gateways reject bare requests); only the common-path guesses on the page's origin are probed with a bare `{__typename}` request.
-- **Build** (`build-enhanced.js`): esbuild bundles both entries as IIFEs; production drops `debugger` statements and treats `console.info`/`console.debug` as pure (stripped) — so use those for dev-only logging and `console.log`/`warn`/`error` for messages that should survive in release builds. The HTML/manifest are patched for the flat `extension_build/` layout; loading the repo root unpacked does not work.
+- **Build** (`build-enhanced.js`): esbuild bundles both entries as IIFEs; production drops `debugger` statements and treats `console.info`/`console.debug` as pure (stripped) — so use those for dev-only logging and `console.log`/`warn`/`error` for messages that should survive in release builds. CSS is an ordered `config.css.inputs` array concatenated then minified once (budgeted in `check-bundle-size.js`); `frontend/fonts/` is copied into the build. The HTML/manifest are patched for the flat `extension_build/` layout; loading the repo root unpacked does not work.
## Tests
diff --git a/DESIGN.md b/DESIGN.md
new file mode 100644
index 0000000..355596b
--- /dev/null
+++ b/DESIGN.md
@@ -0,0 +1,354 @@
+---
+name: APQ Debugger
+description: A DevTools-native panel that recovers the GraphQL query behind every Apollo persisted-query hash.
+colors:
+ surface: "light-dark(#ffffff, #202124)"
+ surface-chrome: "light-dark(#f1f3f4, #292a2d)"
+ surface-raised: "light-dark(#ffffff, #303134)"
+ surface-sunken: "light-dark(#f8f9fa, #1b1c1e)"
+ surface-hover: "light-dark(#eef1f6, #2f3236)"
+ selection: "light-dark(#d3e3fd, #0f3a6e)"
+ selection-idle: "light-dark(#e8eaed, #3c4043)"
+ input-surface: "light-dark(#ffffff, #202124)"
+ divider: "light-dark(#dadce0, #3c4043)"
+ input-border: "light-dark(#80868b, #80868b)"
+ raised-border: "light-dark(#c4c7c5, #5f6368)"
+ ink: "light-dark(#1f1f1f, #e3e3e3)"
+ ink-secondary: "light-dark(#5f6368, #a8acb1)"
+ ink-disabled: "light-dark(#9aa0a6, #6f7378)"
+ ink-on-selection: "light-dark(#041e49, #e3eefe)"
+ devtools-blue: "light-dark(#0b57d0, #a8c7fa)"
+ devtools-blue-hover: "light-dark(#0842a0, #c2d7fb)"
+ on-devtools-blue: "light-dark(#ffffff, #062e6f)"
+ focus: "light-dark(#0b57d0, #a8c7fa)"
+ record-red: "light-dark(#d93025, #f28b82)"
+ resolved-green: "light-dark(#137333, #81c995)"
+ awaiting-amber: "light-dark(#8a5300, #fdd663)"
+ error-red: "light-dark(#b3261e, #f2b8b5)"
+ error-wash: "light-dark(#fce8e6, #3c1b18)"
+ live-wash: "light-dark(#fef7e0, #3a3013)"
+ live-border: "light-dark(#f2d68b, #6b5718)"
+ live-ink: "light-dark(#3c2c00, #fdecb7)"
+ passive-wash: "light-dark(#e8f0fe, #172b4d)"
+ passive-border: "light-dark(#c2d7fb, #2c4a7a)"
+ passive-ink: "light-dark(#062e6f, #d3e3fd)"
+ update-flash: "light-dark(#fde293, #6b5718)"
+ syntax-keyword: "light-dark(#aa0d91, #e36eec)"
+ syntax-type: "light-dark(#0b57d0, #8ab4f8)"
+ syntax-variable: "light-dark(#c5221f, #f28b82)"
+ syntax-string: "light-dark(#1a1aa6, #f29766)"
+ syntax-enum: "light-dark(#1c00cf, #b39ddb)"
+ syntax-comment: "light-dark(#5f6368, #9aa0a6)"
+typography:
+ title:
+ fontFamily: "system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, Cantarell, sans-serif"
+ fontSize: "13px"
+ fontWeight: 600
+ lineHeight: 1.4
+ body:
+ fontFamily: "system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, Cantarell, sans-serif"
+ fontSize: "12px"
+ fontWeight: 400
+ lineHeight: 1.4
+ label:
+ fontFamily: "system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, Cantarell, sans-serif"
+ fontSize: "12px"
+ fontWeight: 500
+ lineHeight: 1.4
+ code:
+ fontFamily: "ui-monospace, Menlo, Consolas, 'Cascadia Mono', 'DejaVu Sans Mono', monospace"
+ fontSize: "12px"
+ fontWeight: 400
+ lineHeight: 1.5
+ code-small:
+ fontFamily: "ui-monospace, Menlo, Consolas, 'Cascadia Mono', 'DejaVu Sans Mono', monospace"
+ fontSize: "11px"
+ fontWeight: 400
+ lineHeight: 1.4
+ fontFeature: "\"tnum\" 1"
+rounded:
+ sm: "3px"
+ md: "4px"
+ lg: "8px"
+ pill: "999px"
+spacing:
+ s-1: "2px"
+ s-2: "4px"
+ s-3: "6px"
+ s-4: "8px"
+ s-5: "12px"
+ s-6: "16px"
+ s-7: "24px"
+components:
+ button-toolbar:
+ backgroundColor: "transparent"
+ textColor: "{colors.ink}"
+ rounded: "{rounded.md}"
+ padding: "0 8px"
+ height: "22px"
+ button-toolbar-hover:
+ backgroundColor: "{colors.surface-hover}"
+ button-primary:
+ backgroundColor: "{colors.devtools-blue}"
+ textColor: "{colors.on-devtools-blue}"
+ typography: "{typography.label}"
+ rounded: "{rounded.md}"
+ padding: "0 8px"
+ height: "22px"
+ button-primary-hover:
+ backgroundColor: "{colors.devtools-blue-hover}"
+ button-danger:
+ backgroundColor: "{colors.error-red}"
+ typography: "{typography.label}"
+ rounded: "{rounded.md}"
+ padding: "0 8px"
+ height: "22px"
+ button-icon:
+ backgroundColor: "transparent"
+ textColor: "{colors.ink-secondary}"
+ rounded: "{rounded.md}"
+ size: "24px"
+ chip-filter:
+ backgroundColor: "transparent"
+ textColor: "{colors.ink}"
+ rounded: "{rounded.pill}"
+ padding: "0 8px"
+ height: "20px"
+ chip-filter-active:
+ backgroundColor: "{colors.selection}"
+ textColor: "{colors.ink-on-selection}"
+ input-toolbar:
+ backgroundColor: "{colors.input-surface}"
+ textColor: "{colors.ink}"
+ rounded: "{rounded.md}"
+ padding: "0 6px"
+ height: "22px"
+ tab:
+ backgroundColor: "transparent"
+ textColor: "{colors.ink-secondary}"
+ padding: "0 12px"
+ height: "28px"
+ tab-active:
+ textColor: "{colors.devtools-blue}"
+ toolbar:
+ backgroundColor: "{colors.surface-chrome}"
+ padding: "2px 6px"
+ height: "30px"
+ table-row:
+ backgroundColor: "{colors.surface}"
+ textColor: "{colors.ink}"
+ typography: "{typography.body}"
+ padding: "0 8px"
+ height: "24px"
+ table-row-selected:
+ backgroundColor: "{colors.selection}"
+ textColor: "{colors.ink-on-selection}"
+ table-row-selected-idle:
+ backgroundColor: "{colors.selection-idle}"
+ infobar-live:
+ backgroundColor: "{colors.live-wash}"
+ textColor: "{colors.live-ink}"
+ padding: "4px 12px"
+ infobar-passive:
+ backgroundColor: "{colors.passive-wash}"
+ textColor: "{colors.passive-ink}"
+ padding: "4px 12px"
+ code-block:
+ backgroundColor: "{colors.surface-sunken}"
+ textColor: "{colors.ink}"
+ typography: "{typography.code}"
+ rounded: "{rounded.md}"
+ padding: "8px 12px"
+ menu:
+ backgroundColor: "{colors.surface-raised}"
+ rounded: "{rounded.lg}"
+ padding: "6px 0"
+ width: "240px"
+ dialog:
+ backgroundColor: "{colors.surface-raised}"
+ rounded: "{rounded.lg}"
+ padding: "16px"
+ width: "400px"
+---
+
+# Design System: APQ Debugger
+
+## Overview
+
+**Creative North Star: "The First-Party Panel"**
+
+APQ Debugger is built to be mistaken for a panel Chrome shipped itself. It sits beside Network and Elements, so it takes their system wholesale: 12px system UI type, monospace for anything machine-made (hashes, queries, URLs, timestamps), white and grey-100 surfaces (dark: #202124 / #292a2d) split by 1px dividers, Google blue for selection, focus and the one primary action, record red, a yellow infobar while traffic is being rewritten, and green reserved for a resolved hash. Familiarity is the design goal; personality is spent only on state changes.
+
+The structure is the DevTools canon: a tab strip (Requests, Schema, settings gear and session status at the right), a per-view toolbar, an optional collapsible patterns pane, an infobar when interception or passive mode is on, then a dense single-line data table with a detail pane that appears only after selection. In the detail pane the recovered query is the largest, most readable thing on screen, wrapped and syntax-coloured in DevTools Sources colours. The system rejects the editorial card-and-metric dashboard and the permanent settings sidebar that preceded it.
+
+Density is a working constraint, not a preference: the panel is often docked about 400px tall. Comfortable matches DevTools' default row height (24px), Compact its dense tables (20px), and Compact switches on by itself when the panel is 460px tall or less. The one signature moment is a pending APQ row resolving in place, its state flipping to Resolved under the DevTools "updated" flash.
+
+**Key Characteristics:**
+- System UI 12px for interface, ui-monospace for code, hashes, URLs and times; no bundled or network fonts.
+- Flat in-flow surfaces separated by 1px dividers and tonal steps (surface vs chrome vs sunken).
+- Blue means selection, focus or primary; red means recording; yellow means traffic is being rewritten; green means resolved.
+- Every request state is a word plus a glyph shape, never colour alone.
+- Single-line 24px (20px compact) table rows with column dividers.
+- Every colour defined once with light-dark(); dark mode only flips color-scheme.
+
+## Colors
+
+A neutral DevTools grey-and-white field with a strict signal vocabulary: one blue, one red, one yellow, one green, each tied to a single meaning.
+
+### Primary
+- **DevTools Blue** (devtools-blue): the active tab label and its 2px underline, primary buttons (Load schema, Copy in the cURL dialog), link buttons and the checkbox accent. Its hover deepens to DevTools Blue Hover.
+- **Focus Blue** (focus): the 2px focus outline on every interactive element and the resize handle's hover/drag state. Same value as DevTools Blue by design; kept as its own token so focus can diverge without touching actions.
+- **Selection Blue** (selection): the selected row, schema type and active filter pill while their list has focus; text on it uses Ink on Selection. When the list loses focus the row drops to Selection Grey (selection-idle), exactly as DevTools does.
+
+### Secondary
+- **Record Red** (record-red): the record circle while recording, the "Intercepting" session status, and the pulsing dot in the live infobar. Nothing else.
+- **Live Yellow** (live-wash, live-border, live-ink): the interception infobar ("Intercepting requests matching ...") and warning toasts. It appears only while requests are actually being rewritten or something needs attention.
+- **Passive Blue Wash** (passive-wash, passive-border, passive-ink): the passive-mode infobar ("Requests are not modified"). Blue-grey, calm, informational.
+
+### Tertiary (state)
+- **Resolved Green** (resolved-green): the Resolved state word and its filled dot.
+- **Awaiting Amber** (awaiting-amber): the Awaiting retry state (hollow ring), the "none" patterns summary warning, and the warning session status.
+- **Error Red** (error-red, error-wash): Not in registry (dash glyph), schema load errors, error toasts, and the destructive confirm button.
+- **Update Flash** (update-flash): the background a row starts from when it resolves in place, fading out over 1.2s.
+
+### Neutral
+- **Surface** (surface): the table, panes and detail body.
+- **Chrome Grey** (surface-chrome): tab strip, toolbars, table head, group headers, code-block headers, detail head, inline code.
+- **Raised** (surface-raised, raised-border): menus and dialogs.
+- **Sunken** (surface-sunken): query, variables and SDL code bodies.
+- **Hover** (surface-hover): hover wash on rows, tabs, buttons and menu options.
+- **Divider** (divider): every 1px region split, column rule and resting input border; row bottoms use it at 55% strength.
+- **Input Border** (input-border): input and select border on hover, chosen to reach 3:1.
+- **Ink** (ink), **Ink Secondary** (ink-secondary), **Ink Disabled** (ink-disabled): primary text, secondary text (labels, column heads, hashes, times; at least 5.3:1 on surface and chrome), disabled controls.
+
+### Syntax (DevTools Sources)
+- **Keyword** (syntax-keyword) magenta, **Type** (syntax-type) blue, **Variable** (syntax-variable) red, **String** (syntax-string) deep blue / orange in dark, **Enum value** (syntax-enum) indigo / lavender, **Comment** (syntax-comment) grey italic. Used in the query and variables blocks and the schema SDL view only.
+
+### Named Rules
+**The One Meaning Rule.** Each signal colour has exactly one job: blue is selection, focus and the primary action; red is recording; yellow is live rewriting or a warning; green is a resolved hash. A colour never borrows another's meaning.
+
+**The Yellow Means Rewriting Rule.** The yellow infobar is visible if and only if the debugger is attached and rewriting matching traffic; it names the patterns in monospace and carries Stop. Passive mode gets the blue infobar instead, because nothing is modified.
+
+**The Define-Once Rule.** Every colour is declared once in tokens.css as `light-dark(light, dark)`. Dark mode is a `color-scheme` flip in theme-dark.css (explicit `data-theme` wins; otherwise the system preference). No component redeclares a colour per theme.
+
+## Typography
+
+**Interface Font:** system-ui (with -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, sans-serif)
+**Code Font:** ui-monospace (with Menlo, Consolas, Cascadia Mono, DejaVu Sans Mono, monospace)
+
+**Character:** The platform's own faces at DevTools sizes. The UI face disappears into Chrome; the monospace face marks everything the machine produced, so a hash or URL is recognisable at a glance.
+
+### Hierarchy
+- **Title** (600, 13px, 1.4): detail pane operation name, empty-state titles, dialog titles. The largest UI text in the panel.
+- **Body** (400, 12px, 1.4): everything by default: toolbar controls, rows, descriptions, menus, infobar copy.
+- **Label** (500, 12px): operation names in rows, state words, column heads, record and primary buttons, menu group labels. Weight, not size, carries emphasis.
+- **Code** (400, 12px mono, 1.5): query, variables and SDL bodies; wraps (`pre-wrap`, `overflow-wrap: anywhere`), tab size 2.
+- **Code Small** (400, 11px mono, tabular numerals): short hashes, times, pattern inputs and summary, endpoint URLs, group headers, schema type names, inline code.
+
+### Named Rules
+**The Machine-Text Rule.** Anything the machine produced (hash, URL, pattern, timestamp, query, type name) is set in the monospace stack; anything a person reads as interface is in system-ui. Numbers that update in place use tabular numerals.
+
+**The Flat Ramp Rule.** The ramp spans 11px to 13px. Hierarchy comes from weight (400/500/600), colour (ink vs ink-secondary) and position, never from display sizes. Sentence case throughout; no uppercase tracking, no eyebrows.
+
+## Layout
+
+A full-height column: tab strip (28px; 26px compact), toolbar (30px; 26px compact, wraps when crowded), optional patterns pane, optional infobar, then a horizontal split filling the rest. The split is the request table plus, only after a row is selected, a detail pane at `clamp(320px, 45%, 720px)` behind a 1px draggable resize handle (6px hit area, keyboard-focusable).
+
+The request table is a four-column grid: Name `minmax(120px, 1fr)`, State 112px, Hash 76px, Time 76px, with 1px column rules and 8px cell padding. Rows group under sticky endpoint headers. The Schema view is a 240px type list beside a scrolling SDL pane.
+
+Spacing runs on a 4px grid with half steps: 2, 4, 6, 8, 12, 16, 24px (`s-1` to `s-7`). Toolbars use 2px/4px gaps; panes pad 8px by 12px; the detail body pads 8px / 12px / 16px bottom with 8px between blocks; empty states and dialogs use the 16 and 24px steps.
+
+Responsive behaviour is width-driven from a ResizeObserver on the app frame:
+- **Narrow (< 640px):** the detail pane becomes a full overlay over the list; the Hash column, the "Patterns" label and pane descriptions drop; the filter input flexes.
+- **Very narrow (< 420px):** the Time column, the resolved count and toolbar separators also drop; the schema type list narrows to 140px.
+- **Short (height <= 460px):** compact density applies automatically.
+
+**The Docked-First Rule.** Design for the panel docked about 400px tall: no chrome may cost more than one toolbar row plus the optional infobar before data rows begin.
+
+## Elevation & Depth
+
+Flat by default. In-flow regions are separated by 1px dividers and three tonal steps (surface, chrome, sunken), never by shadow. A single shadow exists for things that float above the panel: the settings menu, toasts and dialogs. Dialogs add a 35% black backdrop. Focus is an outline, not a shadow.
+
+### Shadow Vocabulary
+- **Overlay** (`box-shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 2px 6px 2px rgb(0 0 0 / 0.15)`): settings menu, toast, dialog panel. Nothing else.
+
+### Named Rules
+**The Overlays-Only Rule.** A shadow means "floating above the panel". Rows, panes, toolbars, code blocks and infobars never carry one.
+
+## Shapes
+
+Small, uniform corners in the DevTools idiom: 3px for inline code, kbd and skeleton bars; 4px for buttons, inputs, selects, code blocks, toasts and the cURL preview; 8px for menus and dialogs; full pill for the type-filter pills and endpoint count badges. Table rows, panes, toolbars and infobars are square, full-bleed bands. Code blocks are 1px-divider bordered boxes whose header and body share the 4px corner (header rounds the top, body the bottom; collapsed, the header rounds all four).
+
+Icons are inline 16px stroke SVGs (1.75 stroke, round caps and joins, `currentColor`), shipped in the markup. State glyphs are 6 to 8px CSS shapes: filled dot, hollow ring, dash, diamond, small square.
+
+## Components
+
+### Buttons
+Quiet toolbar controls that only show a surface when touched.
+- **Shape:** gently squared (4px), 22px tall (20px compact), 8px horizontal padding, 4px icon gap.
+- **Toolbar (default):** transparent with ink text; hover fills with the hover wash. On content surfaces (detail pane, patterns pane, dialog footers) the same button gains a 1px divider outline so it reads as a control.
+- **Primary:** DevTools Blue fill, on-blue text, weight 500; hover deepens. One per view at most (Load schema; Copy in the cURL dialog).
+- **Danger:** error-red fill, weight 500; used only as the accept button of a destructive confirm.
+- **Icon button:** 24px square, ink-secondary glyph, hover wash; disabled drops to ink-disabled.
+- **Record:** the Network panel's circle. Idle: hollow ink-secondary ring, "Record". Recording: filled record-red circle and red "Stop".
+- **Link button:** blue underlined text (underline at 40% until hover); inside the infobar it inherits the bar's ink.
+- **Focus:** 2px focus-blue outline, 1px offset (inset on tabs, rows, lists and code).
+
+### Chips
+- **Style:** the DevTools Network resource-type pills (All / APQ / Full): pill radius, 20px tall, transparent, ink text, hover wash.
+- **State:** active pill fills with Selection Blue and Ink on Selection. In dialogs (cURL shell picker) inactive pills gain a divider outline.
+
+### Inputs / Fields
+- **Style:** 22px (20px compact), input surface, 1px divider border, 4px radius, 6px padding; placeholders in ink-secondary. Pattern and schema URL inputs use Code Small.
+- **Hover:** border strengthens to input-border.
+- **Focus:** border and outline go focus blue, outline inset 1px.
+- **Checkboxes:** native, 14px, accent-colour DevTools Blue.
+
+### Navigation
+- **Tab strip:** chrome-grey band, tabs as 12px-padded text in ink-secondary; hover wash plus ink; selected tab turns DevTools Blue with a 2px underline that overlaps the strip's bottom divider. Right end holds the session status and the settings gear.
+- **Session status:** one word plus an 8px dot. Idle: ink-secondary hollow ring. Intercepting: record-red, filled, weight 600. Passive: blue filled. Warning: amber. Error: error-red filled.
+
+### Request Table (signature)
+- **Head:** chrome-grey, ink-secondary labels at weight 500, 1px column rules.
+- **Rows:** single line, 24px (20px compact), operation name at weight 500 (anonymous operations italic ink-secondary), state word, 8-character hash and time in Code Small ink-secondary. Hover wash; selected rows Selection Blue when the list is focused, Selection Grey otherwise; keyboard focus is a 1px inset focus outline.
+- **States:** Resolved (green filled dot), Awaiting retry (amber hollow ring), Not in registry (error-red dash), APQ retry (secondary diamond, weight 400), Full query (secondary small square, weight 400). Words come from `request-state.js` and are shared with the detail pane.
+- **Resolve in place:** when the APQ retry lands, the pending row is re-rendered where it sits (focus preserved) and its background fades from Update Flash to nothing over 1.2s on the standard ease. With reduced motion the flash becomes a 1px resolved-green inset outline instead.
+- **Group headers:** sticky chrome-grey bands with the endpoint in Code Small and a tabular request count.
+
+### Infobar
+- **Live:** full-width yellow band under the toolbar: pulsing record dot (three pulses, then still), "Intercepting requests matching" followed by each pattern as inline code, an optional first-run note explaining the swap and Chrome's own debugging bar (dismissed with "Got it"), and an outlined Stop button.
+- **Passive:** the same band in passive blue, no dot, states the registry size and that requests are not modified.
+
+### Detail Pane
+- **Head:** chrome-grey row with the operation name in Title and a close icon button.
+- **Body:** facts line (state word, time, links), a one-line state description, a key/value grid (Endpoint, Hash with Copy) in Code Small, Copy as cURL, then collapsible code blocks: Query (min 72px, the dominant element) and Variables. Each block has a chrome-grey summary header with a rotating triangle, a weight-600 title and a borderless Copy that confirms in place ("Copied").
+
+### Overlays
+- **Settings menu:** 240px raised surface, raised border, 8px radius, overlay shadow; fieldset groups split by dividers (Theme, Density, Passive mode) with radio/check options as 26px rows and a keyboard-shortcut hint footer.
+- **Toast:** bottom-right stack, inverted neutral (dark on light theme, light on dark), 4px radius, overlay shadow, rises 6px in over 200ms. Warning and error variants use the live yellow and error washes with a 1px border.
+- **Dialog:** 400px (640px wide variant) raised panel, 16px padding, 8px radius, overlay shadow, 35% black backdrop; Title heading, secondary description, right-aligned footer (outlined Cancel, then primary or danger).
+
+### Empty States and Loading
+- **Empty:** left-aligned block up to 460px wide, Title heading, numbered steps with bold key terms, secondary description; a filtered variant hides the steps.
+- **Schema skeleton:** 12px chrome-grey bars at 100/80/60/70% width with a 1.4s shimmer to the hover wash.
+
+## Do's and Don'ts
+
+### Do:
+- **Do** take every colour from tokens.css and add new ones there as `light-dark(light, dark)` pairs with text at 4.5:1 and meaningful indicators at 3:1 in both themes.
+- **Do** separate regions with 1px dividers and the surface / chrome / sunken tonal steps.
+- **Do** express every request state as a word plus a glyph shape, using the vocabulary in `request-state.js`.
+- **Do** set hashes, URLs, patterns, times and code in the monospace stack, and counts in tabular numerals.
+- **Do** keep controls at the density heights (22/20px controls, 24/20px rows) so compact and the short-dock auto-compact keep working.
+- **Do** keep the query block wrapped and dominant in the detail pane; the recovered query is the largest readable thing on screen.
+- **Do** give every motion a reduced-motion fallback that keeps the meaning (the resolve flash becomes a green outline).
+- **Do** support forced colours: selection maps to Highlight/HighlightText and floating layers get a CanvasText border.
+
+### Don't:
+- **Don't** add shadows to in-flow surfaces; the overlay shadow is for menus, toasts and dialogs only.
+- **Don't** use green for anything except a resolved hash, or yellow except live interception and warnings.
+- **Don't** signal state with colour alone.
+- **Don't** load fonts, icons or scripts from the network, or ship a display face; the platform system-ui and ui-monospace stacks are the type system.
+- **Don't** reintroduce the editorial card-and-metric dashboard, a permanent settings sidebar, uppercase tracked eyebrows or display numerals.
+- **Don't** redeclare colours for dark mode; flip `color-scheme` only.
+- **Don't** let the query scroll horizontally; it wraps.
diff --git a/PRODUCT.md b/PRODUCT.md
new file mode 100644
index 0000000..d01bae5
--- /dev/null
+++ b/PRODUCT.md
@@ -0,0 +1,57 @@
+# Product
+
+
+
+## Platform
+
+web
+
+## Users
+
+Front-end and GraphQL developers (also backend devs and QA) debugging apps that use Apollo Client's Automatic Persisted Queries. They are mid-investigation inside Chrome DevTools, usually with the Network panel open beside this one, often docked at the bottom of the window at a few hundred pixels tall. Their job: see the real operation, variables and endpoint behind an opaque SHA-256 hash, then copy it into an editor, a cURL or a bug report.
+
+## Product Purpose
+
+APQ requests carry only a hash, so the Network panel cannot show what query ran. APQ Debugger attaches the Chrome Debugger to the inspected tab, swaps the hash for a bogus one so the server answers `PersistedQueryNotFound`, and captures the full query Apollo Client retries with. Success: within seconds of browsing, every hashed operation on the page is readable, copyable, and attributable to its endpoint.
+
+## Positioning
+
+It recovers the query from live traffic without access to the app's source, build, or server-side persisted-query store. Passive mode then resolves known hashes from a local registry without touching traffic at all.
+
+## Operating Context
+
+- Lives as a DevTools panel; identifies its tab via `chrome.devtools.inspectedWindow.tabId`; works undocked and docked.
+- Starting interception shows Chrome's own "is debugging this browser" infobar and rewrites matching requests on a real (often production) site.
+- Session flow: set URL patterns → start → browse → read/copy operations → optionally load the schema by introspection → stop.
+- A toolbar action can also start/stop without opening the panel.
+
+## Capabilities and Constraints
+
+- Active mode (hash contamination) vs passive mode (registry lookup, no traffic changes); registry is LRU-capped.
+- Request list with operation-name filter, APQ/full filter, endpoint filter; detail with query, variables, copy, Copy as cURL (Bash/PowerShell); JSON export; history clear.
+- Schema tab: introspect observed endpoints (replaying captured headers) or a typed URL; SDL type browser.
+- Theme (system/light/dark) and density (comfortable/compact) settings.
+- MV3, `minimum_chrome_version` 123. No network-loaded fonts, icons or scripts; everything ships in the bundle.
+- Terminology: "APQ" (hash-only request), "full" (request carrying query text), "resolved" (hash mapped to query).
+
+## Brand Commitments
+
+- Name: APQ Debugger. Icons in `icons/`.
+- Visual direction (confirmed 2026-10-21): DevTools-native. The panel should sit alongside Chrome DevTools' own panels (Network, Elements) as if it belonged there. This replaces the earlier Kora styling.
+
+## Evidence on Hand
+
+- Store copy: `store/store-description.md`; privacy policy and permission justifications in `store/`.
+- No testimonials, user counts or benchmarks exist; do not invent them.
+
+## Product Principles
+
+1. The recovered query is the product. Everything else serves reading and copying it.
+2. Be honest about interception: the user always knows whether traffic is being rewritten, on which patterns.
+3. Belong in DevTools. Earned familiarity beats personality; save emphasis for state changes and the moment a hash resolves.
+4. Work at the docked size people actually use.
+5. Local only: nothing leaves the browser.
+
+## Accessibility & Inclusion
+
+WCAG 2.2 AA: visible focus on every interactive element, full keyboard operation of list/detail/tabs, 4.5:1 text contrast in both themes, forced-colors and reduced-motion support (already present in the codebase).
diff --git a/README.md b/README.md
index d2c7f6a..6597a7e 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,13 @@
apq-debugger/
├── frontend/
│ ├── devtools.html # Main DevTools panel HTML
-│ └── devtools.css # Panel styles (dark/light theme)
+│ ├── css/ # Kora design system layer
+│ │ ├── tokens.css # Color/type/spacing tokens (light-dark() theming)
+│ │ ├── theme-dark.css # Dark-mode gates (explicit + system preference)
+│ │ ├── density.css # Comfortable/Compact density scales
+│ │ └── components.css # Panel components in the Kora idiom
+│ └── fonts/ # Bundled woff2 (Italiana, Albert Sans,
+│ # Spline Sans Mono — all SIL OFL 1.1)
├── js/
│ ├── shared/ # Constants & logger shared by both bundles
│ ├── sw/ # Service worker modules
diff --git a/build-enhanced.js b/build-enhanced.js
index b8ba729..f72533e 100644
--- a/build-enhanced.js
+++ b/build-enhanced.js
@@ -29,7 +29,13 @@ const config = {
},
],
css: {
- input: 'frontend/devtools.css',
+ // Concatenated in order, then minified into a single output file
+ inputs: [
+ 'frontend/css/tokens.css',
+ 'frontend/css/theme-dark.css',
+ 'frontend/css/density.css',
+ 'frontend/css/components.css',
+ ],
output: 'devtools.min.css',
options: {
level: {
@@ -102,17 +108,22 @@ async function bundleJS(entry, outfile) {
// ── CSS minification (Clean-CSS) ──────────────────────────────────
async function minifyCSS() {
- const inputPath = path.join(config.sourceDir, config.css.input);
const outputPath = path.join(config.extDir, config.css.output);
+ const chunks = [];
- if (!(await fs.pathExists(inputPath))) {
- console.log(`❌ CSS not found: ${config.css.input}`);
- return;
+ for (const input of config.css.inputs) {
+ const inputPath = path.join(config.sourceDir, input);
+ if (!(await fs.pathExists(inputPath))) {
+ console.log(`❌ CSS not found: ${input}`);
+ return;
+ }
+ const code = await fs.readFile(inputPath, 'utf8');
+ chunks.push(`/* ── ${input} ── */\n${code}`);
}
- console.log(`🔄 Processing ${config.css.input}...`);
+ console.log(`🔄 Processing ${config.css.inputs.join(', ')}...`);
- const code = await fs.readFile(inputPath, 'utf8');
+ const code = chunks.join('\n');
const cleanCSS = new CleanCSS(config.css.options);
const result = cleanCSS.minify(code);
@@ -126,7 +137,7 @@ async function minifyCSS() {
const minifiedSize = Buffer.byteLength(result.styles, 'utf8');
const reduction = (((originalSize - minifiedSize) / originalSize) * 100).toFixed(1);
- console.log(`✅ ${config.css.input} → ${config.css.output}`);
+ console.log(`✅ ${config.css.inputs.length} CSS file(s) → ${config.css.output}`);
console.log(` ${originalSize} bytes → ${minifiedSize} bytes (${reduction}% reduction)\n`);
}
@@ -206,7 +217,7 @@ async function watch() {
await build();
const watcher = chokidar.watch(
- ['js/**/*.js', 'frontend/*.css', 'frontend/devtools.html', 'manifest.json', 'icons/*'],
+ ['js/**/*.js', 'frontend/css/**/*.css', 'frontend/devtools.html', 'manifest.json', 'icons/*'],
{
ignored: [/\.min\.(js|css)$/, /node_modules/, /extension_build/],
persistent: true,
diff --git a/frontend/css/components.css b/frontend/css/components.css
new file mode 100644
index 0000000..b89944c
--- /dev/null
+++ b/frontend/css/components.css
@@ -0,0 +1,1694 @@
+/* ================================================================
+ APQ Debugger panel components — DevTools-native.
+ Structure: tab strip → per-view toolbar → optional patterns pane →
+ infobar → data table + detail split. 1px dividers separate regions;
+ only floating layers (menus, dialogs, toasts) carry a shadow.
+ ================================================================ */
+
+/* ---- base ---- */
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ height: 100%;
+ margin: 0;
+}
+
+body {
+ font: var(--fs) / var(--lh) var(--font-ui);
+ color: var(--text);
+ background: var(--bg);
+ overflow: hidden;
+ -webkit-font-smoothing: antialiased;
+}
+
+button,
+input,
+select {
+ font: inherit;
+ color: inherit;
+}
+
+button {
+ cursor: pointer;
+}
+
+button:disabled {
+ cursor: default;
+}
+
+code,
+kbd,
+pre {
+ font-family: var(--font-mono);
+}
+
+code {
+ font-size: var(--fs-sm);
+ padding: 0 var(--s-1);
+ border-radius: var(--r-sm);
+ background: var(--bg-chrome);
+}
+
+kbd {
+ font-size: 10px;
+ padding: 0 var(--s-2);
+ border: 1px solid var(--border-raised);
+ border-radius: var(--r-sm);
+ background: var(--bg);
+ white-space: nowrap;
+}
+
+::selection {
+ background: var(--bg-selected);
+ color: var(--text-on-selected);
+}
+
+:focus-visible {
+ outline: 2px solid var(--focus);
+ outline-offset: 1px;
+}
+
+* {
+ scrollbar-width: thin;
+ scrollbar-color: var(--border-raised) transparent;
+}
+
+.hidden {
+ display: none !important;
+}
+
+.icon {
+ width: 16px;
+ height: 16px;
+ flex: none;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.75;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
+.mono {
+ font-family: var(--font-mono);
+}
+
+/* ---- app frame ---- */
+
+.app {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ min-width: 0;
+}
+
+/* ---- tab strip ---- */
+
+.tabstrip {
+ display: flex;
+ align-items: stretch;
+ justify-content: space-between;
+ min-height: var(--tab-h);
+ padding: 0 var(--s-2) 0 0;
+ background: var(--bg-chrome);
+ border-bottom: 1px solid var(--divider);
+}
+
+.tabs {
+ display: flex;
+ align-items: stretch;
+}
+
+.tab {
+ position: relative;
+ padding: 0 var(--s-5);
+ border: 0;
+ background: none;
+ color: var(--text-2);
+ white-space: nowrap;
+}
+
+.tab:hover {
+ color: var(--text);
+ background: var(--bg-hover);
+}
+
+.tab[aria-selected='true'] {
+ color: var(--primary);
+}
+
+.tab[aria-selected='true']::after {
+ content: '';
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: -1px;
+ height: 2px;
+ background: var(--primary);
+}
+
+.tab:focus-visible {
+ outline-offset: -2px;
+}
+
+.tabstrip-end {
+ display: flex;
+ align-items: center;
+ gap: var(--s-4);
+}
+
+/* session status: one word plus a dot */
+.status {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--s-3);
+ color: var(--text-2);
+ white-space: nowrap;
+}
+
+.status-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ border: 1.5px solid currentColor;
+}
+
+.status.active {
+ color: var(--record);
+ font-weight: 600;
+}
+
+.status.active .status-dot,
+.status.error .status-dot {
+ background: currentColor;
+}
+
+.status.info {
+ color: var(--primary);
+}
+
+.status.info .status-dot {
+ background: currentColor;
+}
+
+.status.warning {
+ color: var(--pending);
+}
+
+.status.error {
+ color: var(--error);
+}
+
+/* ---- buttons ---- */
+
+.icon-btn {
+ display: inline-grid;
+ place-items: center;
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ border: 0;
+ border-radius: var(--r-md);
+ background: none;
+ color: var(--text-2);
+}
+
+.icon-btn:hover:not(:disabled) {
+ color: var(--text);
+ background: var(--bg-hover);
+}
+
+.icon-btn:disabled {
+ color: var(--text-disabled);
+}
+
+.tb-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--s-2);
+ height: var(--control-h);
+ min-width: 24px;
+ padding: 0 var(--s-4);
+ border: 1px solid transparent;
+ border-radius: var(--r-md);
+ background: none;
+ color: var(--text);
+ white-space: nowrap;
+}
+
+.tb-btn.icon-btn {
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ justify-content: center;
+ color: var(--text-2);
+}
+
+.tb-btn:hover:not(:disabled) {
+ background: var(--bg-hover);
+}
+
+.tb-btn:disabled {
+ color: var(--text-disabled);
+}
+
+.tb-btn.primary {
+ background: var(--primary);
+ color: var(--on-primary);
+ font-weight: 500;
+}
+
+.tb-btn.primary:hover:not(:disabled) {
+ background: var(--primary-hover);
+}
+
+.tb-btn.danger {
+ background: var(--error);
+ color: light-dark(#ffffff, #3c1b18);
+ font-weight: 500;
+}
+
+.tb-btn.danger:hover {
+ filter: brightness(0.92);
+}
+
+/* buttons that live on content surfaces read as outlined controls */
+.detail-content .tb-btn,
+.modal-footer .tb-btn:not(.primary):not(.danger),
+.pane-row .tb-btn {
+ border-color: var(--divider);
+}
+
+/* record: the Network panel's circle, red while recording */
+.record-btn {
+ font-weight: 500;
+ padding-left: var(--s-3);
+}
+
+.record-btn .icon-record {
+ fill: none;
+ stroke: var(--text-2);
+ stroke-width: 2;
+}
+
+.record-btn.is-recording {
+ color: var(--record);
+}
+
+.record-btn.is-recording .icon-record {
+ fill: var(--record);
+ stroke: var(--record);
+}
+
+.link-btn {
+ padding: 0;
+ border: 0;
+ background: none;
+ color: var(--primary);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);
+}
+
+.link-btn:hover:not(:disabled) {
+ text-decoration-color: currentColor;
+}
+
+.link-btn:disabled {
+ color: var(--text-disabled);
+ text-decoration: none;
+}
+
+/* ---- toolbar ---- */
+
+.view {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-height: 0;
+}
+
+.views {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-height: 0;
+}
+
+.toolbar {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: var(--s-1) var(--s-2);
+ min-height: var(--toolbar-h);
+ padding: var(--s-1) var(--s-3);
+ background: var(--bg-chrome);
+ border-bottom: 1px solid var(--divider);
+}
+
+.tb-sep {
+ width: 1px;
+ height: 16px;
+ margin: 0 var(--s-2);
+ background: var(--divider);
+}
+
+.tb-spacer {
+ flex: 1;
+}
+
+.tb-input,
+.tb-select {
+ height: var(--control-h);
+ min-width: 0;
+ padding: 0 var(--s-3);
+ border: 1px solid var(--divider);
+ border-radius: var(--r-md);
+ background: var(--bg-input);
+ color: var(--text);
+}
+
+.tb-input:hover,
+.tb-select:hover {
+ border-color: var(--border-input);
+}
+
+.tb-input:focus-visible,
+.tb-select:focus-visible {
+ outline-offset: -1px;
+ border-color: var(--focus);
+}
+
+.tb-input::placeholder {
+ color: var(--text-2);
+}
+
+.filter-input {
+ width: 180px;
+ flex: 0 1 180px;
+}
+
+.tb-select {
+ max-width: 220px;
+ padding-right: var(--s-2);
+}
+
+/* type filter: DevTools Network's resource-type pills */
+.segmented {
+ display: inline-flex;
+ gap: var(--s-1);
+}
+
+.chip {
+ height: calc(var(--control-h) - 2px);
+ padding: 0 var(--s-4);
+ border: 1px solid transparent;
+ border-radius: var(--r-pill);
+ background: none;
+ color: var(--text);
+}
+
+.chip:hover {
+ background: var(--bg-hover);
+}
+
+.chip.active {
+ background: var(--bg-selected);
+ color: var(--text-on-selected);
+}
+
+.patterns-toggle {
+ max-width: 260px;
+ padding-right: var(--s-2);
+}
+
+.patterns-toggle-label {
+ color: var(--text-2);
+}
+
+.patterns-summary {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-family: var(--font-mono);
+ font-size: var(--fs-sm);
+}
+
+.patterns-toggle.is-empty .patterns-summary {
+ font-family: var(--font-ui);
+ font-size: var(--fs);
+ color: var(--pending);
+ font-weight: 500;
+}
+
+.icon-chevron {
+ width: 14px;
+ height: 14px;
+ color: var(--text-2);
+ transition: transform var(--t-fast) var(--ease-out);
+}
+
+.patterns-toggle[aria-expanded='true'] .icon-chevron {
+ transform: rotate(180deg);
+}
+
+.request-count {
+ color: var(--text-2);
+ white-space: nowrap;
+ font-variant-numeric: tabular-nums;
+ padding-right: var(--s-2);
+}
+
+/* ---- patterns pane ---- */
+
+.patterns-pane {
+ display: flex;
+ flex-direction: column;
+ gap: var(--s-3);
+ padding: var(--s-4) var(--s-5);
+ background: var(--bg);
+ border-bottom: 1px solid var(--divider);
+}
+
+.patterns-pane[hidden] {
+ display: none;
+}
+
+.pane-row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: var(--s-3) var(--s-4);
+}
+
+.pane-label {
+ color: var(--text-2);
+ white-space: nowrap;
+}
+
+.pattern-list {
+ display: contents;
+}
+
+.pattern-item {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--s-1);
+}
+
+.pattern-item .urlPattern {
+ width: 200px;
+ font-family: var(--font-mono);
+ font-size: var(--fs-sm);
+}
+
+.btn-remove[hidden] {
+ display: none;
+}
+
+.check {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--s-3);
+ font-weight: 500;
+ white-space: nowrap;
+}
+
+.check input,
+.menu-option input {
+ width: 14px;
+ height: 14px;
+ margin: 0;
+ accent-color: var(--primary);
+}
+
+.pane-desc {
+ color: var(--text-2);
+}
+
+.registry-count {
+ color: var(--text-2);
+ font-variant-numeric: tabular-nums;
+}
+
+.registry-count span {
+ color: var(--text);
+ font-weight: 600;
+}
+
+.endpoints-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--s-2) var(--s-5);
+}
+
+.endpoint-item {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--s-3);
+}
+
+.endpoint-info {
+ display: inline-flex;
+ align-items: baseline;
+ gap: var(--s-2);
+ min-width: 0;
+}
+
+.endpoint-url {
+ font-family: var(--font-mono);
+ font-size: var(--fs-sm);
+ max-width: 320px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.endpoint-count {
+ min-width: 18px;
+ padding: 0 var(--s-2);
+ border-radius: var(--r-pill);
+ background: var(--bg-chrome);
+ color: var(--text-2);
+ font-size: var(--fs-sm);
+ text-align: center;
+ font-variant-numeric: tabular-nums;
+}
+
+/* ---- infobar: live interception (yellow) / passive mode (blue) ---- */
+
+.infobar {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--s-4);
+ min-height: var(--toolbar-h);
+ padding: var(--s-2) var(--s-5);
+ border-bottom: 1px solid;
+}
+
+.infobar.is-live {
+ background: var(--live-bg);
+ border-color: var(--live-border);
+ color: var(--live-text);
+}
+
+.infobar.is-passive {
+ background: var(--info-bg);
+ border-color: var(--info-border);
+ color: var(--info-text);
+}
+
+.infobar.is-passive .icon-record {
+ display: none;
+}
+
+.infobar .icon-record {
+ width: 12px;
+ height: 12px;
+ fill: var(--record);
+ stroke: none;
+ animation: live-pulse 1.2s ease-in-out 3;
+}
+
+@keyframes live-pulse {
+ 50% {
+ opacity: 0.35;
+ }
+}
+
+.infobar-text {
+ flex: 1;
+ margin: 0;
+ min-width: 0;
+ padding-top: 1px;
+}
+
+.infobar > .icon-record {
+ margin-top: 4px;
+}
+
+.infobar-text code {
+ background: color-mix(in srgb, currentColor 10%, transparent);
+}
+
+.infobar-note {
+ display: block;
+ margin-top: var(--s-1);
+ opacity: 0.9;
+}
+
+.infobar .link-btn {
+ color: inherit;
+ white-space: nowrap;
+}
+
+.infobar-action {
+ border-color: color-mix(in srgb, currentColor 35%, transparent);
+ font-weight: 500;
+}
+
+.infobar-action:hover:not(:disabled) {
+ background: color-mix(in srgb, currentColor 10%, transparent);
+}
+
+/* ---- requests split ---- */
+
+.split {
+ position: relative;
+ display: flex;
+ flex: 1;
+ min-height: 0;
+}
+
+.list-pane {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-width: 0;
+}
+
+/* table columns shared by the header and every row */
+.grid-head,
+.request-item {
+ display: grid;
+ grid-template-columns: minmax(120px, 1fr) 112px 76px 76px;
+ align-items: center;
+}
+
+.grid-head > *,
+.request-item > * {
+ min-width: 0;
+ padding: 0 var(--s-4);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.grid-head > * + *,
+.request-item > * + * {
+ border-left: 1px solid var(--divider);
+}
+
+.grid-head {
+ height: var(--row-h);
+ flex: none;
+ background: var(--bg-chrome);
+ border-bottom: 1px solid var(--divider);
+ color: var(--text-2);
+ font-weight: 500;
+}
+
+.request-list {
+ position: relative;
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+.request-list:focus-visible {
+ outline-offset: -2px;
+}
+
+.request-item {
+ height: var(--row-h);
+ cursor: default;
+ border-bottom: 1px solid color-mix(in srgb, var(--divider) 55%, transparent);
+}
+
+.request-item > * {
+ line-height: calc(var(--row-h) - 1px);
+}
+
+.request-item:hover {
+ background: var(--bg-hover);
+}
+
+.request-item.selected {
+ background: var(--bg-selected-idle);
+}
+
+.request-list:focus-within .request-item.selected {
+ background: var(--bg-selected);
+ color: var(--text-on-selected);
+}
+
+.request-item:focus-visible {
+ outline: 1px solid var(--focus);
+ outline-offset: -1px;
+}
+
+.request-operation {
+ font-weight: 500;
+}
+
+.is-anonymous {
+ font-style: italic;
+ font-weight: 400;
+ color: var(--text-2);
+}
+
+.request-hash,
+.request-time {
+ font-family: var(--font-mono);
+ font-size: var(--fs-sm);
+ color: var(--text-2);
+ font-variant-numeric: tabular-nums;
+}
+
+.request-list:focus-within .request-item.selected .request-hash,
+.request-list:focus-within .request-item.selected .request-time,
+.request-list:focus-within .request-item.selected .is-anonymous {
+ color: inherit;
+}
+
+/* resolution state: a word with a shape, never colour alone */
+.state {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--s-2);
+ font-weight: 500;
+}
+
+.state::before {
+ content: '';
+ flex: none;
+ width: 7px;
+ height: 7px;
+}
+
+.state--resolved {
+ color: var(--resolved);
+}
+
+.state--resolved::before {
+ border-radius: 50%;
+ background: currentColor;
+}
+
+.state--pending {
+ color: var(--pending);
+}
+
+.state--pending::before {
+ border-radius: 50%;
+ border: 1.5px solid currentColor;
+}
+
+.state--unknown {
+ color: var(--error);
+}
+
+.state--unknown::before {
+ width: 8px;
+ height: 2px;
+ background: currentColor;
+}
+
+.state--full,
+.state--retry {
+ color: var(--text-2);
+ font-weight: 400;
+}
+
+/* a small diamond: no tree, disclosure or checkbox meaning */
+.state--retry::before {
+ width: 6px;
+ height: 6px;
+ background: currentColor;
+ transform: rotate(45deg);
+}
+
+.state--full::before {
+ width: 6px;
+ height: 6px;
+ background: currentColor;
+ border-radius: 1px;
+}
+
+.request-list:focus-within .request-item.selected .state {
+ color: inherit;
+}
+
+/* the signature moment: a hash row resolves in place */
+.request-item.is-updated {
+ animation: row-updated 1.2s var(--ease-out);
+}
+
+@keyframes row-updated {
+ from {
+ background: var(--flash);
+ }
+}
+
+.request-group-header {
+ display: flex;
+ align-items: center;
+ gap: var(--s-3);
+ height: var(--row-h);
+ padding: 0 var(--s-4);
+ background: var(--bg-chrome);
+ border-bottom: 1px solid var(--divider);
+ color: var(--text-2);
+ font-family: var(--font-mono);
+ font-size: var(--fs-sm);
+ position: sticky;
+ top: 0;
+ z-index: 1;
+}
+
+.group-label {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.group-count {
+ margin-left: var(--s-4);
+ font-family: var(--font-ui);
+ font-size: var(--fs);
+ font-variant-numeric: tabular-nums;
+ white-space: nowrap;
+}
+
+/* ---- empty states ---- */
+
+.empty-state,
+.schema-empty {
+ display: flex;
+ flex-direction: column;
+ gap: var(--s-4);
+ max-width: 460px;
+ padding: var(--s-7) var(--s-6);
+ color: var(--text-2);
+}
+
+.empty-title {
+ margin: 0;
+ color: var(--text);
+ font-size: var(--fs-title);
+ font-weight: 600;
+}
+
+.empty-steps {
+ margin: 0;
+ padding-left: 1.4em;
+ display: grid;
+ gap: var(--s-3);
+}
+
+.empty-steps strong {
+ color: var(--text);
+ font-weight: 600;
+}
+
+.empty-desc {
+ margin: 0;
+}
+
+.empty-state.is-filtered .empty-steps,
+.empty-state.is-filtered .empty-desc {
+ display: none;
+}
+
+/* ---- resize handles ---- */
+
+.resize-handle {
+ position: relative;
+ flex: none;
+ width: 1px;
+ background: var(--divider);
+ cursor: col-resize;
+ touch-action: none;
+}
+
+.resize-handle::before {
+ content: '';
+ position: absolute;
+ inset: 0 -3px;
+}
+
+.resize-handle:hover,
+.resize-handle.dragging,
+.resize-handle:focus-visible {
+ background: var(--focus);
+ outline: none;
+}
+
+.resize-handle:focus-visible::after {
+ content: '';
+ position: absolute;
+ inset: 0 -1px;
+ background: var(--focus);
+}
+
+.split:has(#detail-panel.hidden) #resize-handle-right {
+ display: none;
+}
+
+body.resizing {
+ cursor: col-resize;
+ user-select: none;
+}
+
+/* ---- detail panel ---- */
+
+.detail-panel {
+ display: flex;
+ flex-direction: column;
+ width: clamp(320px, 45%, 720px);
+ min-width: 280px;
+ min-height: 0;
+ background: var(--bg);
+}
+
+.detail-head {
+ display: flex;
+ align-items: center;
+ gap: var(--s-3);
+ min-height: var(--row-h);
+ padding: 0 var(--s-2) 0 var(--s-5);
+ background: var(--bg-chrome);
+ border-bottom: 1px solid var(--divider);
+}
+
+.detail-title {
+ flex: 1;
+ min-width: 0;
+ margin: 0;
+ font-size: var(--fs-title);
+ font-weight: 600;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.detail-content {
+ flex: 1;
+ overflow: auto;
+ padding: var(--s-4) var(--s-5) var(--s-6);
+ display: flex;
+ flex-direction: column;
+ gap: var(--s-4);
+}
+
+.detail-placeholder {
+ color: var(--text-2);
+}
+
+.detail-facts {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: var(--s-2) var(--s-5);
+}
+
+.detail-fact {
+ color: var(--text-2);
+}
+
+.detail-note {
+ margin: calc(var(--s-2) * -1) 0 0;
+ color: var(--text-2);
+}
+
+.kv {
+ display: grid;
+ grid-template-columns: max-content minmax(0, 1fr);
+ gap: var(--s-2) var(--s-5);
+ margin: 0;
+}
+
+.kv dt {
+ color: var(--text-2);
+}
+
+.kv dd {
+ margin: 0;
+ min-width: 0;
+ font-size: var(--fs-sm);
+ overflow-wrap: anywhere;
+ user-select: text;
+}
+
+.kv-hash {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--s-3);
+}
+
+.detail-hash {
+ flex: 1;
+ min-width: 0;
+}
+
+.kv-hash .btn-copy {
+ margin-top: -3px;
+}
+
+.detail-actions {
+ display: flex;
+ gap: var(--s-3);
+}
+
+.code-block {
+ position: relative;
+ flex: none;
+ border: 1px solid var(--divider);
+ border-radius: var(--r-md);
+ scroll-margin-top: var(--s-4);
+}
+
+summary.code-header {
+ list-style: none;
+ cursor: pointer;
+ user-select: none;
+ min-height: 26px;
+}
+
+summary.code-header::-webkit-details-marker {
+ display: none;
+}
+
+summary.code-header::before {
+ content: '';
+ flex: none;
+ width: 0;
+ height: 0;
+ border-style: solid;
+ border-width: 4px 0 4px 6px;
+ border-color: transparent transparent transparent var(--text-2);
+ transition: transform var(--t-fast) var(--ease-out);
+}
+
+.code-block[open] > summary.code-header::before {
+ transform: rotate(90deg);
+}
+
+summary.code-header:focus-visible {
+ outline-offset: -2px;
+}
+
+.code-block:not([open]) > summary.code-header {
+ border-bottom: 0;
+ border-radius: var(--r-md);
+}
+
+.code-block:not([open]) > .btn-copy {
+ display: none;
+}
+
+.code-block > .btn-copy {
+ position: absolute;
+ top: 3px;
+ right: var(--s-1);
+}
+
+.code-header {
+ display: flex;
+ align-items: center;
+ gap: var(--s-3);
+ padding: var(--s-1) var(--s-1) var(--s-1) var(--s-4);
+ background: var(--bg-chrome);
+ border-bottom: 1px solid var(--divider);
+ border-radius: var(--r-md) var(--r-md) 0 0;
+}
+
+.code-title {
+ margin: 0;
+ font-size: var(--fs);
+ font-weight: 600;
+}
+
+.btn-copy {
+ height: 20px;
+ padding: 0 var(--s-4);
+}
+
+.code-block .btn-copy {
+ border-color: transparent;
+}
+
+.btn-copy.is-done {
+ color: var(--text-2);
+}
+
+.code {
+ margin: 0;
+ padding: var(--s-4) var(--s-5);
+ background: var(--bg-sunken);
+ font-size: var(--fs-code);
+ line-height: var(--lh-code);
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ tab-size: 2;
+ user-select: text;
+}
+
+.code:focus-visible {
+ outline-offset: -2px;
+}
+
+.code-block .code {
+ border-radius: 0 0 var(--r-md) var(--r-md);
+}
+
+.code-block--query .code {
+ min-height: 72px;
+}
+
+.code-missing {
+ margin: 0;
+ padding: var(--s-4) var(--s-5);
+ color: var(--text-2);
+ background: var(--bg-sunken);
+ border-radius: 0 0 var(--r-md) var(--r-md);
+}
+
+/* GraphQL and SDL syntax (DevTools Sources colours) */
+.code .keyword,
+.schema-sdl .keyword {
+ color: var(--syn-keyword);
+}
+
+.code .type,
+.schema-sdl .type-name,
+.schema-sdl .builtin {
+ color: var(--syn-type);
+}
+
+.code .variable {
+ color: var(--syn-variable);
+}
+
+.code .string,
+.schema-sdl .string {
+ color: var(--syn-string);
+}
+
+.schema-sdl .field {
+ color: var(--syn-property);
+}
+
+.schema-sdl .enum-value,
+.schema-sdl .number {
+ color: var(--syn-number);
+}
+
+.schema-sdl .punct {
+ color: var(--syn-punct);
+}
+
+.schema-sdl .directive {
+ color: var(--syn-variable);
+}
+
+.schema-sdl .comment {
+ color: var(--syn-comment);
+ font-style: italic;
+}
+
+/* ---- schema view ---- */
+
+.schema-url-input {
+ flex: 1 1 280px;
+ font-family: var(--font-mono);
+ font-size: var(--fs-sm);
+}
+
+.schema-search {
+ width: 160px;
+ flex: 0 1 160px;
+}
+
+.schema-status {
+ padding: var(--s-2) var(--s-5);
+ color: var(--text-2);
+ border-bottom: 1px solid var(--divider);
+ min-height: var(--row-h);
+}
+
+.schema-status:empty {
+ display: none;
+}
+
+.schema-status.is-error {
+ color: var(--error);
+ background: var(--error-bg);
+}
+
+.schema-skeleton {
+ display: grid;
+ gap: var(--s-4);
+ padding: var(--s-5);
+}
+
+.skeleton {
+ height: 12px;
+ border-radius: var(--r-sm);
+ background: linear-gradient(
+ 90deg,
+ var(--bg-chrome) 0%,
+ var(--bg-hover) 50%,
+ var(--bg-chrome) 100%
+ );
+ background-size: 200% 100%;
+ animation: skeleton 1.4s ease-in-out infinite;
+}
+
+.skeleton:nth-child(2) {
+ width: 80%;
+}
+
+.skeleton:nth-child(3) {
+ width: 60%;
+}
+
+.skeleton:nth-child(4) {
+ width: 70%;
+}
+
+@keyframes skeleton {
+ to {
+ background-position: -200% 0;
+ }
+}
+
+.schema-explorer {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-height: 0;
+}
+
+.schema-meta {
+ padding: var(--s-2) var(--s-5);
+ background: var(--bg-chrome);
+ border-bottom: 1px solid var(--divider);
+ color: var(--text-2);
+ font-size: var(--fs-sm);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.schema-body {
+ display: flex;
+ flex: 1;
+ min-height: 0;
+}
+
+.schema-type-list {
+ width: 240px;
+ min-width: 140px;
+ overflow-y: auto;
+ padding: var(--s-1) 0;
+}
+
+.schema-type-list:focus-visible {
+ outline-offset: -2px;
+}
+
+.schema-type-item {
+ display: flex;
+ align-items: baseline;
+ gap: var(--s-3);
+ width: 100%;
+ height: var(--row-h);
+ padding: 0 var(--s-4);
+ border: 0;
+ background: none;
+ text-align: left;
+ white-space: nowrap;
+}
+
+.schema-type-item:hover {
+ background: var(--bg-hover);
+}
+
+.schema-type-item.selected {
+ background: var(--bg-selected-idle);
+}
+
+.schema-type-list:focus-within .schema-type-item.selected {
+ background: var(--bg-selected);
+ color: var(--text-on-selected);
+}
+
+.schema-type-item:focus-visible {
+ outline: 1px solid var(--focus);
+ outline-offset: -1px;
+}
+
+.schema-type-item .kind {
+ width: 64px;
+ flex: none;
+ color: var(--text-2);
+ font-size: var(--fs-sm);
+}
+
+.schema-type-item .name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-family: var(--font-mono);
+ font-size: var(--fs-sm);
+}
+
+.schema-type-list:focus-within .schema-type-item.selected .kind {
+ color: inherit;
+}
+
+.schema-type-list-empty {
+ padding: var(--s-4) var(--s-5);
+ color: var(--text-2);
+}
+
+.schema-sdl {
+ flex: 1;
+ min-width: 0;
+ margin: 0;
+ padding: var(--s-4) var(--s-5);
+ overflow: auto;
+ font-size: var(--fs-code);
+ line-height: var(--lh-code);
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ user-select: text;
+}
+
+.schema-sdl:focus-visible {
+ outline-offset: -2px;
+}
+
+/* ---- settings menu ---- */
+
+.settings-wrap {
+ position: relative;
+}
+
+.menu {
+ position: absolute;
+ top: calc(100% + var(--s-2));
+ right: 0;
+ z-index: 20;
+ width: 240px;
+ padding: var(--s-3) 0;
+ background: var(--bg-raised);
+ border: 1px solid var(--border-raised);
+ border-radius: var(--r-lg);
+ box-shadow: var(--shadow-menu);
+}
+
+.menu-group {
+ margin: 0;
+ padding: var(--s-2) 0;
+ border: 0;
+}
+
+.menu-group + .menu-group {
+ border-top: 1px solid var(--divider);
+}
+
+/* floated so the legend sits inside the group instead of on its border */
+.menu-label {
+ float: left;
+ width: 100%;
+ padding: var(--s-2) var(--s-5);
+ color: var(--text-2);
+ font-weight: 500;
+}
+
+.menu-label + * {
+ clear: both;
+}
+
+.menu-option {
+ display: flex;
+ align-items: center;
+ gap: var(--s-4);
+ height: 26px;
+ padding: 0 var(--s-5);
+ cursor: pointer;
+}
+
+.menu-option:hover {
+ background: var(--bg-hover);
+}
+
+.menu-hint {
+ margin: 0;
+ padding: var(--s-4) var(--s-5) var(--s-2);
+ border-top: 1px solid var(--divider);
+ color: var(--text-2);
+ line-height: 1.7;
+}
+
+/* ---- toasts ---- */
+
+.toast-stack {
+ position: fixed;
+ right: var(--s-5);
+ bottom: var(--s-5);
+ z-index: 40;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: var(--s-3);
+ max-width: min(420px, calc(100vw - 24px));
+ pointer-events: none;
+}
+
+.toast {
+ display: flex;
+ align-items: center;
+ gap: var(--s-4);
+ padding: var(--s-3) var(--s-3) var(--s-3) var(--s-5);
+ min-height: 32px;
+ background: light-dark(#303134, #e3e3e3);
+ color: light-dark(#f1f3f4, #1f1f1f);
+ border-radius: var(--r-md);
+ box-shadow: var(--shadow-menu);
+ pointer-events: auto;
+ animation: toast-in var(--t-med) var(--ease-out);
+}
+
+.toast-text {
+ flex: 1;
+}
+
+.toast--warning {
+ background: var(--live-bg);
+ color: var(--live-text);
+ border: 1px solid var(--live-border);
+}
+
+.toast--error {
+ background: var(--error-bg);
+ color: var(--error);
+ border: 1px solid color-mix(in srgb, var(--error) 40%, transparent);
+}
+
+.toast-close {
+ color: inherit;
+}
+
+.toast-close:hover {
+ background: color-mix(in srgb, currentColor 12%, transparent) !important;
+ color: inherit !important;
+}
+
+@keyframes toast-in {
+ from {
+ opacity: 0;
+ transform: translateY(6px);
+ }
+}
+
+/* ---- dialogs ---- */
+
+.modal {
+ position: fixed;
+ inset: 0;
+ z-index: 50;
+ display: grid;
+ /* a bounded column: an auto track would grow to the widest unbreakable
+ line (a one-line cURL --data-raw) and push the dialog off-screen */
+ grid-template-columns: minmax(0, 1fr);
+ place-items: center;
+ padding: var(--s-5);
+}
+
+.modal-backdrop {
+ position: absolute;
+ inset: 0;
+ background: rgb(0 0 0 / 0.35);
+}
+
+.modal-panel {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: var(--s-4);
+ width: min(400px, 100%);
+ max-height: calc(100vh - 32px);
+ padding: var(--s-6);
+ background: var(--bg-raised);
+ border: 1px solid var(--border-raised);
+ border-radius: var(--r-lg);
+ box-shadow: var(--shadow-menu);
+}
+
+.modal-wide {
+ width: min(640px, 100%);
+}
+
+.modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.modal-title {
+ margin: 0;
+ font-size: var(--fs-title);
+ font-weight: 600;
+}
+
+.modal-desc {
+ margin: 0;
+ color: var(--text-2);
+}
+
+.modal-options .chip {
+ border-color: var(--divider);
+}
+
+.modal-options .chip.active {
+ border-color: transparent;
+}
+
+.modal-preview {
+ max-height: 50vh;
+ overflow: auto;
+ border: 1px solid var(--divider);
+ border-radius: var(--r-md);
+ white-space: pre;
+}
+
+.modal-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: var(--s-3);
+}
+
+/* ---- narrow and short panels ---- */
+
+/* the detail pane overlays the list when there is no room for a split */
+.app.narrow .detail-panel {
+ position: absolute;
+ inset: 0;
+ z-index: 5;
+ width: auto !important;
+ min-width: 0 !important;
+}
+
+.app.narrow #resize-handle-right {
+ display: none;
+}
+
+.app.narrow .grid-head,
+.app.narrow .request-item {
+ grid-template-columns: minmax(100px, 1fr) 112px 76px;
+}
+
+.app.narrow .col-hash {
+ display: none !important;
+}
+
+.app.narrow .patterns-toggle-label,
+.app.narrow .pane-desc {
+ display: none;
+}
+
+.app.narrow .filter-input {
+ flex: 1 1 120px;
+}
+
+.app.very-narrow .grid-head,
+.app.very-narrow .request-item {
+ grid-template-columns: minmax(100px, 1fr) 112px;
+}
+
+.app.very-narrow .col-time,
+.app.very-narrow .count-resolved,
+.app.very-narrow .tb-sep {
+ display: none !important;
+}
+
+.app.very-narrow .schema-type-list {
+ width: 140px !important;
+}
+
+/* ---- accessibility adjustments ---- */
+
+@media (prefers-reduced-motion: reduce) {
+ .request-item.is-updated,
+ .infobar .icon-record,
+ .skeleton,
+ .toast {
+ animation: none;
+ }
+
+ /* the resolved row still marks the change, without movement */
+ .request-item.is-updated {
+ outline: 1px solid var(--resolved);
+ outline-offset: -1px;
+ }
+
+ .icon-chevron,
+ summary.code-header::before {
+ transition: none;
+ }
+}
+
+@media (prefers-contrast: more) {
+ :root {
+ --divider: light-dark(#80868b, #9aa0a6);
+ --text-2: light-dark(#3c4043, #dadce0);
+ }
+}
+
+@media (forced-colors: active) {
+ :focus-visible,
+ .request-item:focus-visible,
+ .schema-type-item:focus-visible {
+ outline: 2px solid Highlight;
+ outline-offset: -2px;
+ }
+
+ .request-item.selected,
+ .schema-type-item.selected,
+ .chip.active,
+ .tab[aria-selected='true'] {
+ forced-color-adjust: none;
+ background: Highlight;
+ color: HighlightText;
+ }
+
+ .tab[aria-selected='true']::after,
+ .status-dot,
+ .state::before {
+ background: CanvasText;
+ }
+
+ .infobar,
+ .toast,
+ .menu,
+ .modal-panel {
+ border: 1px solid CanvasText;
+ }
+}
diff --git a/frontend/css/density.css b/frontend/css/density.css
new file mode 100644
index 0000000..3d49fc9
--- /dev/null
+++ b/frontend/css/density.css
@@ -0,0 +1,38 @@
+/* ================================================================
+ Density. Comfortable matches DevTools' default row height;
+ Compact matches its dense tables. Compact also applies on its own
+ when the panel is docked short, so a 400px dock still shows a
+ useful number of rows. settings.js sets data-density.
+ ================================================================ */
+
+:root {
+ --fs: 12px;
+ --fs-sm: 11px;
+ --fs-code: 12px;
+ --fs-title: 13px;
+ --lh: 1.4;
+ --lh-code: 1.5;
+ --toolbar-h: 30px;
+ --tab-h: 28px;
+ --control-h: 22px;
+ --row-h: 24px;
+ --pad: var(--s-5);
+}
+
+:root[data-density='compact'] {
+ --toolbar-h: 26px;
+ --tab-h: 26px;
+ --control-h: 20px;
+ --row-h: 20px;
+ --pad: var(--s-4);
+}
+
+@media (max-height: 460px) {
+ :root {
+ --toolbar-h: 26px;
+ --tab-h: 26px;
+ --control-h: 20px;
+ --row-h: 20px;
+ --pad: var(--s-4);
+ }
+}
diff --git a/frontend/css/theme-dark.css b/frontend/css/theme-dark.css
new file mode 100644
index 0000000..5e417d2
--- /dev/null
+++ b/frontend/css/theme-dark.css
@@ -0,0 +1,16 @@
+/* ================================================================
+ Dark theme gates. All dark values live in tokens.css as the second
+ argument of light-dark(); these gates only flip the color scheme.
+ An explicit data-theme wins; the system preference applies
+ otherwise (settings.js removes the attribute for "system").
+ ================================================================ */
+
+:root[data-theme='dark'] {
+ color-scheme: dark;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root:not([data-theme='light']) {
+ color-scheme: dark;
+ }
+}
diff --git a/frontend/css/tokens.css b/frontend/css/tokens.css
new file mode 100644
index 0000000..2e677eb
--- /dev/null
+++ b/frontend/css/tokens.css
@@ -0,0 +1,104 @@
+/* ================================================================
+ APQ Debugger — DevTools-native tokens.
+ The panel sits beside Chrome's own Network and Elements panels, so
+ it borrows their system: system UI type at 12px, ui-monospace for
+ code and hashes, white / grey surfaces split by 1px dividers,
+ Google blue for selection, focus and primary actions, record red,
+ a yellow infobar while traffic is being rewritten, and green only
+ for a resolved hash.
+
+ Theming: every color token is defined ONCE via light-dark().
+ theme-dark.css only flips color-scheme. Text pairs are >= 4.5:1
+ on the surface they sit on; indicators and borders >= 3:1 where
+ they carry meaning.
+ ================================================================ */
+
+:root {
+ color-scheme: light;
+
+ /* ---- type ---- */
+ --font-ui: system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, Cantarell, sans-serif;
+ --font-mono: ui-monospace, Menlo, Consolas, 'Cascadia Mono', 'DejaVu Sans Mono', monospace;
+
+ /* ---- surfaces ---- */
+ --bg: light-dark(#ffffff, #202124);
+ --bg-chrome: light-dark(#f1f3f4, #292a2d); /* tab strip, toolbars, table head */
+ --bg-raised: light-dark(#ffffff, #303134); /* menus, dialogs */
+ --bg-sunken: light-dark(#f8f9fa, #1b1c1e); /* code blocks */
+ --bg-hover: light-dark(#eef1f6, #2f3236);
+ --bg-selected: light-dark(#d3e3fd, #0f3a6e);
+ --bg-selected-idle: light-dark(#e8eaed, #3c4043); /* selected row, list not focused */
+ --bg-input: light-dark(#ffffff, #202124);
+
+ /* ---- lines ---- */
+ --divider: light-dark(#dadce0, #3c4043);
+ --border-input: light-dark(#80868b, #80868b); /* 3:1+ on bg and chrome */
+ --border-raised: light-dark(#c4c7c5, #5f6368);
+
+ /* ---- text ---- */
+ --text: light-dark(#1f1f1f, #e3e3e3);
+ --text-2: light-dark(#5f6368, #a8acb1); /* secondary: >= 5.3:1 on bg and chrome */
+ --text-disabled: light-dark(#9aa0a6, #6f7378);
+ --text-on-selected: light-dark(#041e49, #e3eefe);
+
+ /* ---- accent and state ---- */
+ --primary: light-dark(#0b57d0, #a8c7fa);
+ --primary-hover: light-dark(#0842a0, #c2d7fb);
+ --on-primary: light-dark(#ffffff, #062e6f);
+ --focus: light-dark(#0b57d0, #a8c7fa);
+
+ --record: light-dark(#d93025, #f28b82);
+
+ --resolved: light-dark(#137333, #81c995);
+ --resolved-bg: light-dark(#e6f4ea, #10301b);
+ --pending: light-dark(#8a5300, #fdd663);
+ --pending-bg: light-dark(#fef7e0, #3a2f0b);
+ --error: light-dark(#b3261e, #f2b8b5);
+ --error-bg: light-dark(#fce8e6, #3c1b18);
+
+ /* live interception infobar (DevTools "paused" yellow) */
+ --live-bg: light-dark(#fef7e0, #3a3013);
+ --live-border: light-dark(#f2d68b, #6b5718);
+ --live-text: light-dark(#3c2c00, #fdecb7);
+
+ /* passive-mode infobar */
+ --info-bg: light-dark(#e8f0fe, #172b4d);
+ --info-border: light-dark(#c2d7fb, #2c4a7a);
+ --info-text: light-dark(#062e6f, #d3e3fd);
+
+ /* row "updated" flash, as in the Elements panel */
+ --flash: light-dark(#fde293, #6b5718);
+
+ /* ---- syntax (DevTools Sources) ---- */
+ --syn-keyword: light-dark(#aa0d91, #e36eec);
+ --syn-type: light-dark(#0b57d0, #8ab4f8);
+ --syn-variable: light-dark(#c5221f, #f28b82);
+ --syn-property: light-dark(#994500, #e8c38a);
+ --syn-string: light-dark(#1a1aa6, #f29766);
+ --syn-number: light-dark(#1c00cf, #b39ddb);
+ --syn-punct: light-dark(#5f6368, #a8acb1);
+ --syn-comment: light-dark(#5f6368, #9aa0a6);
+
+ /* ---- space (4px grid) ---- */
+ --s-1: 2px;
+ --s-2: 4px;
+ --s-3: 6px;
+ --s-4: 8px;
+ --s-5: 12px;
+ --s-6: 16px;
+ --s-7: 24px;
+
+ /* ---- shape ---- */
+ --r-sm: 3px;
+ --r-md: 4px;
+ --r-lg: 8px;
+ --r-pill: 999px;
+
+ /* ---- elevation: overlays only (menus, dialogs, toasts) ---- */
+ --shadow-menu: 0 1px 2px rgb(0 0 0 / 0.3), 0 2px 6px 2px rgb(0 0 0 / 0.15);
+
+ /* ---- motion ---- */
+ --ease-out: cubic-bezier(0.2, 0, 0, 1);
+ --t-fast: 120ms;
+ --t-med: 200ms;
+}
diff --git a/frontend/devtools.css b/frontend/devtools.css
deleted file mode 100644
index 5ae7693..0000000
--- a/frontend/devtools.css
+++ /dev/null
@@ -1,1700 +0,0 @@
-/* ================================================
- APQ Debugger - DevTools Panel Styles
- System-aware theming with light/dark mode
- ================================================ */
-
-/* CSS Custom Properties - Light Mode (Default) */
-:root {
- --color-primary: #4F46E5;
- --color-primary-hover: #4338CA;
- --color-success: #16A34A;
- --color-success-bg: #DCFCE7;
- --color-warning: #D97706;
- --color-warning-bg: #FEF3C7;
- --color-error: #DC2626;
- --color-error-bg: #FEE2E2;
-
- --color-surface: #FFFFFF;
- --color-surface-alt: #F4F4F5;
- --color-surface-hover: #E4E4E7;
- --color-border: #E4E4E7;
- --color-border-strong: #D4D4D8;
-
- --color-text-primary: #18181B;
- --color-text-secondary: #52525B;
- --color-text-muted: #71717A;
- --color-text-inverse: #FFFFFF;
-
- --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
- --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
- --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
-
- --radius-sm: 4px;
- --radius-md: 8px;
- --radius-lg: 12px;
- --radius-full: 9999px;
-
- --font-mono: 'SF Mono', 'Fira Code', 'Consolas', monospace;
- --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-
- --transition-fast: 0.15s ease;
- --transition-normal: 0.2s ease;
-
- /* Density (overridden by [data-density="compact"]) */
- --font-size-base: 13px;
- --header-height: 40px;
- --control-pad-y: 8px;
- --item-pad-y: 10px;
- --panel-pad: 12px;
- --section-pad: 10px;
-}
-
-/* Compact density */
-:root[data-density="compact"] {
- --font-size-base: 12px;
- --header-height: 32px;
- --control-pad-y: 5px;
- --item-pad-y: 5px;
- --panel-pad: 8px;
- --section-pad: 6px;
-}
-
-/* Dark Mode.
- The variable block is intentionally duplicated: once for the explicit
- user override ([data-theme="dark"]) and once for system preference
- (when no explicit "light" override is set). */
- :root[data-theme="dark"] {
- --color-primary: #6366F1;
- --color-primary-hover: #818CF8;
- --color-success: #22C55E;
- --color-success-bg: rgba(34, 197, 94, 0.15);
- --color-warning: #F59E0B;
- --color-warning-bg: rgba(245, 158, 11, 0.15);
- --color-error: #EF4444;
- --color-error-bg: rgba(239, 68, 68, 0.15);
- --color-surface: #1E1E2E;
- --color-surface-alt: #2A2A3E;
- --color-surface-hover: #3A3A4E;
- --color-border: #3A3A4E;
- --color-border-strong: #4A4A5E;
- --color-text-primary: #E4E4E7;
- --color-text-secondary: #A1A1AA;
- --color-text-muted: #71717A;
- --color-text-inverse: #18181B;
- --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
- --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
- --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5);
-}
-
-/* Dark Mode */
-@media (prefers-color-scheme: dark) {
- :root:not([data-theme="light"]) {
- --color-primary: #6366F1;
- --color-primary-hover: #818CF8;
- --color-success: #22C55E;
- --color-success-bg: rgba(34, 197, 94, 0.15);
- --color-warning: #F59E0B;
- --color-warning-bg: rgba(245, 158, 11, 0.15);
- --color-error: #EF4444;
- --color-error-bg: rgba(239, 68, 68, 0.15);
-
- --color-surface: #1E1E2E;
- --color-surface-alt: #2A2A3E;
- --color-surface-hover: #3A3A4E;
- --color-border: #3A3A4E;
- --color-border-strong: #4A4A5E;
-
- --color-text-primary: #E4E4E7;
- --color-text-secondary: #A1A1AA;
- --color-text-muted: #71717A;
- --color-text-inverse: #18181B;
-
- --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
- --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
- --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5);
- }
-}
-
-/* Reset & Base */
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
-}
-
-body {
- font-family: var(--font-sans);
- font-size: var(--font-size-base);
- line-height: 1.5;
- color: var(--color-text-primary);
- background: var(--color-surface);
- min-height: 100vh;
- overflow: hidden;
-}
-
-/* App Container */
-.app {
- display: flex;
- flex-direction: column;
- height: 100vh;
- overflow: hidden;
-}
-
-/* ================================================
- Header
- ================================================ */
-.header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- height: var(--header-height);
- padding: 0 12px;
- background: var(--color-surface-alt);
- border-bottom: 1px solid var(--color-border);
- flex-shrink: 0;
-}
-
-.header-left {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.header-logo {
- width: 20px;
- height: 20px;
-}
-
-.header-title {
- font-weight: 600;
- font-size: 14px;
- color: var(--color-text-primary);
-}
-
-.header-center {
- display: flex;
- align-items: center;
-}
-
-.header-right {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-/* Status Badge */
-.status-badge {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 4px 10px;
- border-radius: var(--radius-full);
- background: var(--color-surface);
- border: 1px solid var(--color-border);
- font-size: 12px;
- font-weight: 500;
-}
-
-.status-badge.active {
- background: var(--color-success-bg);
- border-color: var(--color-success);
-}
-
-.status-badge.active .status-dot {
- background: var(--color-success);
- animation: pulse 2s infinite;
-}
-
-.status-badge.warning {
- background: var(--color-warning-bg);
- border-color: var(--color-warning);
-}
-
-.status-badge.warning .status-dot {
- background: var(--color-warning);
-}
-
-.status-badge.error {
- background: var(--color-error-bg);
- border-color: var(--color-error);
-}
-
-.status-badge.error .status-dot {
- background: var(--color-error);
-}
-
-.status-dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background: var(--color-text-muted);
-}
-
-@keyframes pulse {
-
- 0%,
- 100% {
- opacity: 1;
- }
-
- 50% {
- opacity: 0.5;
- }
-}
-
-/* Request Count */
-.request-count {
- display: flex;
- align-items: baseline;
- gap: 4px;
- font-size: 12px;
- color: var(--color-text-muted);
-}
-
-.count-number {
- font-weight: 700;
- font-size: 14px;
- color: var(--color-primary);
-}
-
-/* ================================================
- Main Content Layout
- ================================================ */
-.main-content {
- display: flex;
- flex: 1;
- overflow: hidden;
-}
-
-/* ================================================
- Sidebar - Patterns
- ================================================ */
-.sidebar {
- width: 220px;
- min-width: 180px;
- display: flex;
- flex-direction: column;
- background: var(--color-surface);
- overflow: hidden;
-}
-
-.sidebar-header {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: var(--panel-pad);
- border-bottom: 1px solid var(--color-border);
-}
-
-.sidebar-title {
- font-size: 12px;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: var(--color-text-muted);
-}
-
-.patterns-list {
- flex: 1;
- overflow-y: auto;
- padding: 8px;
-}
-
-.pattern-item {
- display: flex;
- align-items: center;
- gap: 4px;
- margin-bottom: 6px;
-}
-
-.urlPattern {
- flex: 1;
- min-width: 0;
- padding: var(--control-pad-y) 10px;
- border: 1px solid var(--color-border);
- border-radius: var(--radius-md);
- background: var(--color-surface);
- color: var(--color-text-primary);
- font-size: 12px;
- font-family: var(--font-mono);
- transition: border-color var(--transition-fast);
-}
-
-.urlPattern:focus {
- outline: none;
- border-color: var(--color-primary);
-}
-
-.urlPattern::placeholder {
- color: var(--color-text-muted);
-}
-
-.btn-add-pattern {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 4px;
- margin: 0 8px 8px;
- padding: 8px;
- border: 1px dashed var(--color-border);
- border-radius: var(--radius-md);
- background: transparent;
- color: var(--color-text-muted);
- font-size: 12px;
- cursor: pointer;
- transition: all var(--transition-fast);
-}
-
-.btn-add-pattern:hover {
- border-color: var(--color-primary);
- color: var(--color-primary);
- background: var(--color-surface-alt);
-}
-
-.btn-icon-plus {
- font-size: 14px;
- font-weight: 300;
-}
-
-.sidebar-actions {
- padding: 8px;
- border-top: 1px solid var(--color-border);
-}
-
-/* ================================================
- Status Banner
- ================================================ */
-.status-banner {
- margin: 0 8px 8px;
- padding: 8px 10px;
- border-radius: var(--radius-md);
- background: var(--color-surface-alt);
- border: 1px solid var(--color-border);
- font-size: 11px;
- line-height: 1.4;
- color: var(--color-text-secondary);
- transition: all var(--transition-normal);
- word-break: break-word;
-}
-
-.status-banner.hidden {
- display: none;
-}
-
-.status-banner.success {
- background: var(--color-success-bg);
- border-color: var(--color-success);
- color: var(--color-success);
-}
-
-.status-banner.warning {
- background: var(--color-warning-bg);
- border-color: var(--color-warning);
- color: var(--color-warning);
-}
-
-.status-banner.error {
- background: var(--color-error-bg);
- border-color: var(--color-error);
- color: var(--color-error);
-}
-
-.status-banner-text {
- display: block;
-}
-
-/* ================================================
- Sidebar Sections (Passive Mode, Schema)
- ================================================ */
-.sidebar-section {
- padding: var(--section-pad) 8px;
- border-top: 1px solid var(--color-border);
-}
-
-.sidebar-section-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
-}
-
-.sidebar-section-title {
- font-size: 11px;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: var(--color-text-secondary);
-}
-
-.sidebar-section-desc {
- margin-top: 4px;
- font-size: 11px;
- line-height: 1.4;
- color: var(--color-text-muted);
-}
-
-/* Toggle switch */
-.toggle-switch {
- position: relative;
- display: inline-block;
- width: 32px;
- height: 18px;
- flex-shrink: 0;
-}
-
-.toggle-switch input {
- opacity: 0;
- width: 0;
- height: 0;
-}
-
-.toggle-slider {
- position: absolute;
- inset: 0;
- background: var(--color-border-strong);
- border-radius: var(--radius-full);
- cursor: pointer;
- transition: background var(--transition-normal);
-}
-
-.toggle-slider::before {
- content: '';
- position: absolute;
- width: 14px;
- height: 14px;
- left: 2px;
- top: 2px;
- background: var(--color-surface);
- border-radius: 50%;
- box-shadow: var(--shadow-sm);
- transition: transform var(--transition-normal);
-}
-
-.toggle-switch input:checked+.toggle-slider {
- background: var(--color-primary);
-}
-
-.toggle-switch input:checked+.toggle-slider::before {
- transform: translateX(14px);
-}
-
-.toggle-switch input:focus-visible+.toggle-slider {
- outline: 2px solid var(--color-primary);
- outline-offset: 2px;
-}
-
-/* Registry info row */
-.registry-info {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-top: 8px;
- font-size: 11px;
- color: var(--color-text-secondary);
-}
-
-.btn-link {
- border: none;
- background: none;
- padding: 0;
- font-size: 11px;
- color: var(--color-primary);
- cursor: pointer;
-}
-
-.btn-link:hover {
- color: var(--color-primary-hover);
- text-decoration: underline;
-}
-
-.btn-link:disabled {
- color: var(--color-text-muted);
- cursor: default;
- text-decoration: none;
-}
-
-/* Schema form */
-.schema-load-bar {
- display: flex;
- flex-direction: column;
- align-items: stretch;
- gap: 8px;
- padding: 8px var(--panel-pad);
- border-bottom: 1px solid var(--color-border);
- flex-shrink: 0;
- min-width: 0;
-}
-
-.schema-actions {
- display: flex;
- align-items: stretch;
- gap: 6px;
- width: 100%;
- min-width: 0;
-}
-
-.schema-url-input {
- width: 100%;
- min-width: 0;
- box-sizing: border-box;
- padding: 6px 8px;
- border: 1px solid var(--color-border-strong);
- border-radius: var(--radius-sm);
- background: var(--color-surface);
- color: var(--color-text-primary);
- font-size: 11px;
- font-family: var(--font-mono);
-}
-
-.schema-actions .schema-search,
-.schema-actions .btn {
- flex: 1;
- min-width: 0;
- box-sizing: border-box;
-}
-
-.schema-actions .schema-search {
- padding: 5px 8px;
- border: 1px solid var(--color-border);
- border-radius: var(--radius-md);
- background: var(--color-surface);
- color: var(--color-text-primary);
- font-size: 12px;
-}
-
-.schema-url-input:focus,
-.schema-actions .schema-search:focus {
- outline: none;
- border-color: var(--color-primary);
-}
-
-.schema-status {
- font-size: 11px;
- color: var(--color-text-muted);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- min-width: 0;
- width: 100%;
-}
-
-/* ================================================
- Buttons
- ================================================ */
-.btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 6px;
- padding: var(--control-pad-y) 16px;
- border: none;
- border-radius: var(--radius-md);
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
- transition: all var(--transition-fast);
-}
-
-.btn-block {
- width: 100%;
-}
-
-.btn-primary {
- background: var(--color-primary);
- color: var(--color-text-inverse);
-}
-
-.btn-primary:hover {
- background: var(--color-primary-hover);
-}
-
-.btn-danger {
- background: var(--color-error);
- color: var(--color-text-inverse);
-}
-
-.btn-danger:hover {
- background: #B91C1C;
-}
-
-.btn-success {
- background: var(--color-success);
- color: var(--color-text-inverse);
-}
-
-.btn-icon {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 24px;
- height: 24px;
- border: none;
- border-radius: var(--radius-sm);
- background: transparent;
- color: var(--color-text-muted);
- font-size: 16px;
- cursor: pointer;
- transition: all var(--transition-fast);
-}
-
-.btn-icon:hover {
- background: var(--color-surface-hover);
- color: var(--color-text-primary);
-}
-
-.btn-remove {
- color: var(--color-error);
-}
-
-.btn-remove:hover {
- background: var(--color-error-bg);
-}
-
-.btn-icon-play {
- font-size: 10px;
-}
-
-/* ================================================
- Request Panel
- ================================================ */
-.request-panel {
- flex: 1;
- display: flex;
- flex-direction: column;
- background: var(--color-surface);
- overflow: hidden;
- min-width: 300px;
-}
-
-.panel-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: var(--section-pad) var(--panel-pad);
- border-bottom: 1px solid var(--color-border);
- gap: 12px;
- flex-wrap: wrap;
-}
-
-.panel-title {
- font-size: 12px;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: var(--color-text-muted);
- white-space: nowrap;
-}
-
-.main-panel-header {
- flex-wrap: nowrap;
-}
-
-.main-panel-header .panel-toolbar {
- margin-left: auto;
-}
-
-.panel-toolbar.hidden {
- display: none;
-}
-
-.main-tab-content {
- flex: 1;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- min-height: 0;
-}
-
-.main-tab-content.hidden {
- display: none;
-}
-
-.filter-bar {
- display: flex;
- align-items: center;
- gap: 8px;
- justify-content: flex-end;
- padding: 8px var(--panel-pad);
- border-bottom: 1px solid var(--color-border);
- flex-shrink: 0;
-}
-
-.main-tab-content .request-list {
- flex: 1;
- min-height: 0;
-}
-
-.filter-input {
- width: 180px;
- padding: 6px 10px;
- border: 1px solid var(--color-border);
- border-radius: var(--radius-md);
- background: var(--color-surface);
- color: var(--color-text-primary);
- font-size: 12px;
- transition: border-color var(--transition-fast);
-}
-
-.filter-input:focus {
- outline: none;
- border-color: var(--color-primary);
-}
-
-.filter-chips {
- display: flex;
- gap: 4px;
-}
-
-.chip {
- padding: 4px 10px;
- border: 1px solid var(--color-border);
- border-radius: var(--radius-full);
- background: var(--color-surface);
- color: var(--color-text-muted);
- font-size: 11px;
- font-weight: 500;
- cursor: pointer;
- transition: all var(--transition-fast);
-}
-
-.chip:hover {
- border-color: var(--color-primary);
- color: var(--color-primary);
-}
-
-.chip.active {
- background: var(--color-primary);
- border-color: var(--color-primary);
- color: var(--color-text-inverse);
-}
-
-/* Request List */
-.request-list {
- flex: 1;
- overflow-y: auto;
- padding: 4px;
-}
-
-.request-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: var(--item-pad-y) 12px;
- border-radius: var(--radius-md);
- cursor: pointer;
- transition: background var(--transition-fast);
- border: 1px solid transparent;
-}
-
-.request-item:hover {
- background: var(--color-surface-alt);
-}
-
-.request-item.selected {
- background: var(--color-surface-alt);
- border-color: var(--color-primary);
-}
-
-.request-info {
- flex: 1;
- min-width: 0;
-}
-
-.request-operation {
- font-weight: 600;
- font-size: 13px;
- color: var(--color-text-primary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.request-url {
- font-size: 11px;
- color: var(--color-text-muted);
- font-family: var(--font-mono);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.request-meta {
- display: flex;
- align-items: center;
- gap: 8px;
- flex-shrink: 0;
-}
-
-.request-time {
- font-size: 11px;
- color: var(--color-text-muted);
- font-family: var(--font-mono);
-}
-
-.request-badge {
- padding: 2px 6px;
- border-radius: var(--radius-sm);
- font-size: 10px;
- font-weight: 700;
- text-transform: uppercase;
-}
-
-.request-badge.apq {
- background: var(--color-primary);
- color: var(--color-text-inverse);
-}
-
-.request-badge.full {
- background: var(--color-success);
- color: var(--color-text-inverse);
-}
-
-/* Empty State */
-.empty-state {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- height: 100%;
- padding: 40px;
- text-align: center;
-}
-
-.empty-icon {
- font-size: 48px;
- margin-bottom: 16px;
- opacity: 0.5;
-}
-
-.empty-title {
- font-size: 14px;
- font-weight: 600;
- color: var(--color-text-secondary);
- margin-bottom: 4px;
-}
-
-.empty-desc {
- font-size: 12px;
- color: var(--color-text-muted);
- max-width: 240px;
-}
-
-/* ================================================
- Detail Panel
- ================================================ */
-.detail-panel {
- width: 350px;
- min-width: 280px;
- display: flex;
- flex-direction: column;
- background: var(--color-surface);
- overflow: hidden;
-}
-
-.detail-panel.hidden {
- display: none;
-}
-
-.detail-content {
- flex: 1;
- overflow-y: auto;
- padding: var(--panel-pad);
-}
-
-.detail-placeholder {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 100%;
- color: var(--color-text-muted);
- font-size: 12px;
-}
-
-/* Detail Sections */
-.detail-section {
- margin-bottom: 16px;
-}
-
-.detail-label {
- font-size: 10px;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: var(--color-text-muted);
- margin-bottom: 6px;
-}
-
-.detail-value {
- font-size: 13px;
- color: var(--color-text-primary);
- word-break: break-all;
-}
-
-.detail-hash {
- font-family: var(--font-mono);
- font-size: 11px;
- color: var(--color-text-secondary);
- background: var(--color-surface-alt);
- padding: 8px;
- border-radius: var(--radius-md);
- word-break: break-all;
-}
-
-/* Code Block */
-.code-block {
- position: relative;
- background: var(--color-surface-alt);
- border-radius: var(--radius-md);
- overflow: hidden;
-}
-
-.code-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 6px 10px;
- background: var(--color-surface-hover);
- border-bottom: 1px solid var(--color-border);
-}
-
-.code-title {
- font-size: 10px;
- font-weight: 600;
- text-transform: uppercase;
- color: var(--color-text-muted);
-}
-
-.btn-copy {
- padding: 2px 8px;
- font-size: 10px;
-}
-
-.code-content {
- padding: 12px;
- font-family: var(--font-mono);
- font-size: 12px;
- line-height: 1.6;
- color: var(--color-text-primary);
- white-space: pre-wrap;
- word-break: break-word;
- max-height: 300px;
- overflow-y: auto;
-}
-
-/* Syntax Highlighting */
-.code-content .keyword {
- color: #C678DD;
-}
-
-.code-content .type {
- color: #E5C07B;
-}
-
-.code-content .field {
- color: #61AFEF;
-}
-
-.code-content .string {
- color: #98C379;
-}
-
-.code-content .variable {
- color: #E06C75;
-}
-
-/* ================================================
- Focus Styles (Keyboard Accessibility)
- ================================================ */
-*:focus-visible {
- outline: 2px solid var(--color-primary);
- outline-offset: 2px;
-}
-
-/* Remove outline for mouse users */
-*:focus:not(:focus-visible) {
- outline: none;
-}
-
-.request-item:focus-visible {
- outline: 2px solid var(--color-primary);
- outline-offset: -2px;
- background: var(--color-surface-alt);
-}
-
-.request-item[aria-selected="true"] {
- background: var(--color-surface-alt);
- border-color: var(--color-primary);
-}
-
-.chip:focus-visible {
- outline: 2px solid var(--color-primary);
- outline-offset: 2px;
-}
-
-.request-list:focus-visible {
- outline: none;
-}
-
-/* ================================================
- Panel Toolbar (Clear / Export)
- ================================================ */
-.panel-toolbar {
- display: flex;
- align-items: center;
- gap: 4px;
- flex-shrink: 0;
-}
-
-.btn-toolbar {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 4px 10px;
- border: 1px solid var(--color-border);
- border-radius: var(--radius-md);
- background: var(--color-surface);
- color: var(--color-text-secondary);
- font-size: 11px;
- font-weight: 500;
- cursor: pointer;
- transition: all var(--transition-fast);
-}
-
-.btn-toolbar:hover:not(:disabled) {
- border-color: var(--color-primary);
- color: var(--color-primary);
- background: var(--color-surface-alt);
-}
-
-.btn-toolbar:disabled {
- opacity: 0.4;
- cursor: not-allowed;
-}
-
-.btn-toolbar span[aria-hidden] {
- font-size: 12px;
-}
-
-/* ================================================
- Resize Handles
- ================================================ */
-.resize-handle {
- width: 5px;
- cursor: col-resize;
- background: transparent;
- position: relative;
- flex-shrink: 0;
- z-index: 10;
- transition: background var(--transition-fast);
-}
-
-.resize-handle::after {
- content: '';
- position: absolute;
- top: 0;
- bottom: 0;
- left: 2px;
- width: 1px;
- background: var(--color-border);
- transition: background var(--transition-fast), width var(--transition-fast);
-}
-
-.resize-handle:hover::after,
-.resize-handle.dragging::after {
- left: 1px;
- width: 3px;
- background: var(--color-primary);
- border-radius: 2px;
-}
-
-.resize-handle:focus-visible::after {
- left: 1px;
- width: 3px;
- background: var(--color-primary);
- border-radius: 2px;
-}
-
-/* Hide the old border on panels when resize handles are present */
-.resize-handle-left + .request-panel {
- border-left: none;
-}
-
-/* Prevent text selection while dragging */
-body.resizing {
- cursor: col-resize !important;
- user-select: none;
-}
-
-body.resizing * {
- cursor: col-resize !important;
-}
-
-/* ================================================
- Improved Empty & Filter States
- ================================================ */
-.empty-state.filter-empty .empty-icon {
- font-size: 36px;
-}
-
-/* ================================================
- Scrollbar Styling
- ================================================ */
-::-webkit-scrollbar {
- width: 8px;
- height: 8px;
-}
-
-::-webkit-scrollbar-track {
- background: transparent;
-}
-
-::-webkit-scrollbar-thumb {
- background: var(--color-border);
- border-radius: 4px;
-}
-
-::-webkit-scrollbar-thumb:hover {
- background: var(--color-border-strong);
-}
-
-/* ================================================
- Inline SVG Icons
- ================================================ */
-.icon {
- width: 14px;
- height: 14px;
- flex-shrink: 0;
- vertical-align: -2px;
- fill: none;
- stroke: currentColor;
- stroke-width: 1.8;
- stroke-linecap: round;
- stroke-linejoin: round;
-}
-
-.empty-icon .icon {
- width: 42px;
- height: 42px;
- stroke-width: 1.2;
-}
-
-.panel-title .icon,
-.sidebar-title .icon {
- margin-right: 4px;
-}
-
-/* ================================================
- Header Buttons & Settings Popover
- ================================================ */
-.btn-header {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 26px;
- height: 26px;
- border: none;
- border-radius: var(--radius-md);
- background: transparent;
- color: var(--color-text-muted);
- cursor: pointer;
- transition: all var(--transition-fast);
-}
-
-.btn-header:hover,
-.btn-header[aria-expanded="true"] {
- background: var(--color-surface-hover);
- color: var(--color-text-primary);
-}
-
-.settings-wrap {
- position: relative;
-}
-
-.settings-popover {
- position: absolute;
- top: calc(100% + 6px);
- right: 0;
- z-index: 100;
- width: 220px;
- padding: 12px;
- border: 1px solid var(--color-border);
- border-radius: var(--radius-lg);
- background: var(--color-surface);
- box-shadow: var(--shadow-lg);
-}
-
-.settings-popover.hidden {
- display: none;
-}
-
-.settings-group+.settings-group {
- margin-top: 12px;
-}
-
-.settings-group-label {
- display: block;
- font-size: 10px;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: var(--color-text-muted);
- margin-bottom: 6px;
-}
-
-.settings-options {
- display: flex;
- gap: 4px;
-}
-
-.settings-option {
- flex: 1;
-}
-
-.settings-option input {
- position: absolute;
- opacity: 0;
- pointer-events: none;
-}
-
-.settings-option span {
- display: block;
- padding: 5px 0;
- border: 1px solid var(--color-border);
- border-radius: var(--radius-md);
- text-align: center;
- font-size: 11px;
- font-weight: 500;
- color: var(--color-text-secondary);
- cursor: pointer;
- transition: all var(--transition-fast);
-}
-
-.settings-option input:checked+span {
- background: var(--color-primary);
- border-color: var(--color-primary);
- color: var(--color-text-inverse);
-}
-
-.settings-option input:focus-visible+span {
- outline: 2px solid var(--color-primary);
- outline-offset: 2px;
-}
-
-/* ================================================
- Panel Tabs (Requests / Schema)
- ================================================ */
-.panel-tabs {
- display: flex;
- gap: 2px;
- padding: 2px;
- border-radius: var(--radius-md);
- background: var(--color-surface-alt);
- flex-shrink: 0;
-}
-
-.panel-tab {
- padding: 4px 12px;
- border: none;
- border-radius: var(--radius-sm);
- background: transparent;
- color: var(--color-text-muted);
- font-size: 11px;
- font-weight: 600;
- cursor: pointer;
- transition: all var(--transition-fast);
-}
-
-.panel-tab[aria-selected="true"] {
- background: var(--color-surface);
- color: var(--color-text-primary);
- box-shadow: var(--shadow-sm);
-}
-
-.panel-tab:disabled {
- opacity: 0.4;
- cursor: not-allowed;
-}
-
-/* ================================================
- Schema Viewer
- ================================================ */
-#schema-content {
- min-height: 0;
-}
-
-#schema-content.hidden {
- display: none;
-}
-
-.schema-explorer {
- flex: 1;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- min-height: 0;
-}
-
-.schema-explorer.hidden {
- display: none;
-}
-
-.schema-body {
- flex: 1;
- display: flex;
- overflow: hidden;
- min-height: 0;
-}
-
-.schema-type-list {
- width: 220px;
- min-width: 140px;
- flex-shrink: 0;
- overflow-y: auto;
- padding: 4px 0;
-}
-
-.schema-type-item {
- display: flex;
- align-items: baseline;
- gap: 6px;
- width: 100%;
- padding: 4px 10px;
- border: none;
- background: transparent;
- text-align: left;
- font-family: var(--font-mono);
- font-size: 11px;
- color: var(--color-text-secondary);
- cursor: pointer;
- min-width: 0;
-}
-
-.schema-type-item:hover {
- background: var(--color-surface-hover);
- color: var(--color-text-primary);
-}
-
-.schema-type-item.selected {
- background: var(--color-surface-alt);
- color: var(--color-text-primary);
- font-weight: 600;
-}
-
-.schema-type-item .kind {
- flex: 0 0 6em;
- font-size: 10px;
- font-weight: 600;
- text-transform: uppercase;
- color: var(--color-text-muted);
-}
-
-.schema-type-item .name {
- min-width: 0;
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.schema-type-list-empty {
- padding: 16px 10px;
- font-size: 11px;
- color: var(--color-text-muted);
- text-align: center;
-}
-
-.schema-types {
- flex: 1;
- overflow-y: auto;
- padding: var(--panel-pad);
-}
-
-#schema-sdl-view {
- flex: 1;
- min-width: 0;
- margin: 0;
- padding: 12px var(--panel-pad);
- overflow: auto;
- border: none;
- border-radius: 0;
- background: transparent;
-}
-
-.schema-sdl {
- margin: 0;
- padding: 12px;
- border-radius: var(--radius-md);
- background: var(--color-surface-alt);
- font-family: var(--font-mono);
- font-size: 11px;
- line-height: 1.55;
- color: var(--color-text-primary);
- white-space: pre-wrap;
- word-break: break-word;
-}
-
-.schema-sdl .keyword {
- color: #C678DD;
-}
-
-.schema-sdl .builtin {
- color: #E5C07B;
-}
-
-.schema-sdl .type-name {
- color: #E5C07B;
- font-weight: 500;
-}
-
-.schema-sdl .field {
- color: #61AFEF;
-}
-
-.schema-sdl .directive {
- color: #C678DD;
- font-style: italic;
-}
-
-.schema-sdl .string {
- color: #98C379;
-}
-
-.schema-sdl .comment {
- color: #5C6370;
- font-style: italic;
-}
-
-.schema-sdl .enum-value {
- color: #D19A66;
-}
-
-.schema-sdl .punct {
- color: #ABB2BF;
-}
-
-.schema-meta {
- padding: 6px var(--panel-pad);
- font-size: 11px;
- color: var(--color-text-muted);
- border-bottom: 1px solid var(--color-border);
- word-break: break-all;
-}
-
-.schema-empty {
- padding: 24px;
- text-align: center;
- font-size: 12px;
- color: var(--color-text-muted);
-}
-
-/* ================================================
- Responsive Layout (class-driven, set by layout.js)
- ================================================ */
-.main-content {
- position: relative;
-}
-
-/* Manual sidebar collapse (any width) */
-.app.sidebar-collapsed .sidebar,
-.app.sidebar-collapsed #resize-handle-left {
- display: none;
-}
-
-/* Narrow: detail panel becomes an overlay */
-.app.narrow .detail-panel {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- z-index: 20;
- width: min(85%, 360px);
- border-left: 1px solid var(--color-border);
- box-shadow: var(--shadow-lg);
-}
-
-.app.narrow #resize-handle-right {
- display: none;
-}
-
-.app.narrow .request-panel {
- min-width: 0;
-}
-
-.app.narrow .schema-body {
- flex-direction: column;
-}
-
-.app.narrow .schema-type-list {
- width: auto !important;
- min-width: 0;
- max-height: 38%;
- flex-shrink: 0;
- border-bottom: 1px solid var(--color-border);
-}
-
-.app.narrow #resize-handle-schema {
- display: none;
-}
-
-.app.narrow #schema-sdl-view {
- flex: 1;
- min-height: 0;
-}
-
-/* Very narrow: sidebar becomes a slide-in drawer */
-.app.very-narrow .sidebar {
- position: absolute;
- top: 0;
- left: 0;
- bottom: 0;
- z-index: 30;
- width: min(85%, 260px);
- border-right: 1px solid var(--color-border);
- box-shadow: var(--shadow-lg);
- transform: translateX(-105%);
- transition: transform var(--transition-normal);
-}
-
-.app.very-narrow.sidebar-open .sidebar {
- transform: none;
-}
-
-.app.very-narrow #resize-handle-left {
- display: none;
-}
-
-.app.very-narrow .filter-input {
- width: 110px;
-}
-
-.app.very-narrow .header-title {
- display: none;
-}
-
-/* ================================================
- Detail actions
- ================================================ */
-.detail-actions {
- display: flex;
- gap: 8px;
-}
-
-/* ================================================
- Modal (Copy as cURL)
- ================================================ */
-.modal {
- position: fixed;
- inset: 0;
- z-index: 1000;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 16px;
-}
-
-.modal.hidden {
- display: none;
-}
-
-.modal-backdrop {
- position: absolute;
- inset: 0;
- background: rgba(0, 0, 0, 0.45);
-}
-
-.modal-panel {
- position: relative;
- z-index: 1;
- width: min(560px, 100%);
- max-height: min(80vh, 520px);
- display: flex;
- flex-direction: column;
- border: 1px solid var(--color-border);
- border-radius: var(--radius-lg);
- background: var(--color-surface);
- box-shadow: var(--shadow-lg);
-}
-
-.modal-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 12px 14px;
- border-bottom: 1px solid var(--color-border);
-}
-
-.modal-title {
- font-size: 14px;
- font-weight: 600;
- color: var(--color-text-primary);
-}
-
-.modal-desc {
- padding: 10px 14px 0;
- font-size: 12px;
- color: var(--color-text-muted);
-}
-
-.modal-options {
- display: flex;
- gap: 6px;
- padding: 10px 14px;
-}
-
-.curl-shell-option {
- flex: 1;
- padding: 6px 10px;
- border: 1px solid var(--color-border);
- border-radius: var(--radius-md);
- background: var(--color-surface-alt);
- color: var(--color-text-secondary);
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
- transition: all var(--transition-fast);
-}
-
-.curl-shell-option.active,
-.curl-shell-option[aria-pressed="true"] {
- background: var(--color-primary);
- border-color: var(--color-primary);
- color: var(--color-text-inverse);
-}
-
-.modal-preview {
- margin: 0 14px;
- padding: 10px 12px;
- border: 1px solid var(--color-border);
- border-radius: var(--radius-md);
- background: var(--color-surface-alt);
- font-family: var(--font-mono);
- font-size: 11px;
- line-height: 1.5;
- color: var(--color-text-primary);
- white-space: pre-wrap;
- word-break: break-all;
- overflow: auto;
- max-height: 240px;
- flex: 1;
-}
-
-.modal-footer {
- display: flex;
- justify-content: flex-end;
- padding: 12px 14px;
- border-top: 1px solid var(--color-border);
-}
-
-/* ================================================
- Reduced Motion
- ================================================ */
-@media (prefers-reduced-motion: reduce) {
-
- *,
- *::before,
- *::after {
- transition-duration: 0.01ms !important;
- animation-duration: 0.01ms !important;
- animation-iteration-count: 1 !important;
- }
-}
\ No newline at end of file
diff --git a/frontend/devtools.html b/frontend/devtools.html
index db59dbd..fd907dc 100644
--- a/frontend/devtools.html
+++ b/frontend/devtools.html
@@ -1,280 +1,520 @@
-
+
-
-
-
-
+
+
+
APQ Debugger
-
-
+
+
-
+
-
-