SPB Git forge

spb/trawls

Public
3commits 1branches 0releases
456.0 KBsize
maindefault branch
18 days agolast push
Python 76.2% JavaScript 11.3% CSS 6.3% HTML 5.9%

feat(ui): thème clair par défaut + bascule sombre, refonte responsive smartphone (barre d'onglets, options repliables, tailles tactiles, safe-area)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 18 days ago (Sep 5, 2026) parent 62aa5ae

3 changed files +262 −109

modified trawls/web/static/app.js +11 −1
@@ -33,6 +33,8 @@
33 33 return `<span class="${cls}">${m}</span>`;
34 34 });
35 35 const md = (s) => (window.marked ? marked.parse(s || '', { mangle: false, headerIds: false }) : `<pre>${esc(s)}</pre>`);
36 + const isMobile = () => window.matchMedia('(max-width: 900px)').matches;
37 + const scrollToEl = (sel) => { if (!isMobile()) return; const el = $(sel); if (el) setTimeout(() => el.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50); };
36 38
37 39 // ---- routeur -------------------------------------------------------------------------
38 40 const routes = { playground: renderPlayground, crawls: renderCrawls, map: renderMap, extract: renderExtract, keys: renderKeys, agent: renderAgent };
@@ -41,7 +43,8 @@
41 43 if (push) history.pushState({}, '', path);
42 44 const seg = path.split('/').filter(Boolean);
43 45 const name = routes[seg[0]] ? seg[0] : 'playground';
44 − $$('#nav a').forEach((a) => a.classList.toggle('on', a.dataset.route === name));
46 + $$('#nav a, #tabbar a').forEach((a) => a.classList.toggle('on', a.dataset.route === name));
47 + window.scrollTo(0, 0);
45 48 if (cleanup) { try { cleanup(); } catch {} cleanup = null; }
46 49 view.innerHTML = '';
47 50 view.appendChild($(`#tpl-${name}`).content.cloneNode(true));
@@ -61,6 +64,9 @@
61 64 } catch { el.className = 'status err'; el.lastElementChild.textContent = 'offline'; }
62 65 }
63 66 $('#btn-key').onclick = () => { $('#key-input').value = getKey(); $('#dlg-key').showModal(); };
67 + // thème : clair par défaut, bascule persistée
68 + const applyTheme = (t) => { document.documentElement.setAttribute('data-theme', t); try { localStorage.setItem('trawls.theme', t); } catch {} const m = document.querySelector('meta[name=theme-color]:not([media])') || Object.assign(document.head.appendChild(document.createElement('meta')), { name: 'theme-color' }); m.content = t === 'dark' ? '#111317' : '#ffffff'; };
69 + $('#btn-theme').onclick = () => applyTheme(document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
64 70 $('#key-save').onclick = () => { setKey($('#key-input').value.trim()); toast('Clé enregistrée'); };
65 71
66 72 // ---- snippets -----------------------------------------------------------------------------
@@ -82,6 +88,7 @@
82 88 $('#pg-hist').onclick = (e) => { const li = e.target.closest('li[data-i]'); if (!li) return; const h = hist()[+li.dataset.i]; $('#pg-url').value = h.url; run(); };
83 89 $('#pg-hist-clear').onclick = () => { localStorage.removeItem(HIST); drawHist(); };
84 90 drawHist();
91 + if (isMobile()) $('#pg-opts').open = false;
85 92
86 93 const buildBody = () => {
87 94 const formats = $$('#pg-formats input:checked').map((i) => i.value);
@@ -131,6 +138,7 @@
131 138 if (p.status === 'ok' && body.extract && tab === 'md') tab = 'json';
132 139 $$('#pg-tabs button[data-tab]').forEach((x) => x.classList.toggle('on', x.dataset.tab === tab));
133 140 draw();
141 + scrollToEl('#pg-tabs');
134 142 const h = hist().filter((x) => x.url !== body.url); h.unshift({ url: body.url, status: p.status, at: new Date().toISOString() }); localStorage.setItem(HIST, JSON.stringify(h.slice(0, 20))); drawHist();
135 143 } catch (e) {
136 144 $('#pg-bar').innerHTML = `<span class="err">✕ ${esc(e.code || 'ERREUR')}: ${esc(e.message)}</span>`; $('#pg-result').innerHTML = `<pre class="code mono">${hl({ error: { code: e.code, message: e.message, details: e.details } })}</pre>`;
@@ -156,6 +164,7 @@
156 164 };
157 165 $('#cr-jobs').onclick = (e) => { const d = e.target.closest('[data-del]'); if (d) { api(`/v1/jobs/${d.dataset.del}`, { method: 'DELETE' }).then(list); return; } const tr = e.target.closest('tr[data-id]'); if (tr) open(tr.dataset.id); };
158 166 $('#cr-refresh').onclick = list;
167 + if (isMobile()) $('#cr-opts').open = false;
159 168 $('#cr-form').onsubmit = async (e) => {
160 169 e.preventDefault();
161 170 let body;
@@ -178,6 +187,7 @@
178 187 async function open(id) {
179 188 current = id; $$('#cr-jobs tr').forEach((tr) => tr.classList.toggle('sel', tr.dataset.id === id));
180 189 history.replaceState({}, '', `/crawls/${id}`);
190 + scrollToEl('#cr-detail');
181 191 if (es) { es.close(); es = null; }
182 192 const d = $('#cr-detail');
183 193 d.innerHTML = `<div class="result-bar mono" id="cd-bar">chargement…</div><div class="progress"><i id="cd-prog" style="width:0%"></i></div>
modified trawls/web/static/index.html +52 −22
@@ -1,23 +1,40 @@
1 1 <!doctype html>
2 −<html lang="fr" data-theme="dark">
2 +<html lang="fr" data-theme="light">
3 3 <head>
4 4 <meta charset="utf-8">
5 5 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6 6 <title>Trawls — crawl, extract, navigate</title>
7 7 <meta name="description" content="Trawls : moteur de crawling, d'extraction et de navigation web, sortie Markdown LLM-ready. Self-host first.">
8 +<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
9 +<meta name="theme-color" content="#111317" media="(prefers-color-scheme: dark)">
10 +<meta name="apple-mobile-web-app-capable" content="yes">
11 +<meta name="apple-mobile-web-app-status-bar-style" content="default">
12 +<meta name="apple-mobile-web-app-title" content="Trawls">
8 13 <meta property="og:title" content="Trawls — crawl, extract, navigate">
9 14 <meta property="og:description" content="Un chalut ratisse le fond et remonte tout : on cartographie et on extrait n'importe quel site. Markdown propre, escalade http → navigateur → stealth, API OpenAPI.">
10 15 <meta property="og:image" content="https://www.trawls.dev/static/og.svg">
11 16 <meta property="og:url" content="https://www.trawls.dev/">
12 17 <link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
18 +<link rel="apple-touch-icon" href="/static/favicon.svg">
19 +<script>(function(){try{var t=localStorage.getItem('trawls.theme');if(t==='dark'||t==='light'){document.documentElement.setAttribute('data-theme',t);}}catch(e){}})();</script>
13 20 <link rel="preconnect" href="https://fonts.googleapis.com">
14 21 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
15 22 <link rel="stylesheet" href="/static/style.css">
16 23 </head>
17 24 <body>
25 +<svg width="0" height="0" style="position:absolute" aria-hidden="true">
26 + <symbol id="i-play" viewBox="0 0 24 24"><path d="M8 5v14l11-7z" fill="currentColor"/></symbol>
27 + <symbol id="i-crawl" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M3 7c4 0 5 5 9 5s5-5 9-5M3 13c4 0 5 5 9 5s5-5 9-5"/></symbol>
28 + <symbol id="i-map" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"><path d="M3 6l6-2 6 2 6-2v14l-6 2-6-2-6 2zM9 4v14M15 6v14"/></symbol>
29 + <symbol id="i-extract" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M4 5h16M4 12h10M4 19h16M18 9v6M15 12h6"/></symbol>
30 + <symbol id="i-key" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="8" cy="15" r="4"/><path d="M11 12l9-9M15 5l3 3M18 8l2-2"/></symbol>
31 + <symbol id="i-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></symbol>
32 + <symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"><path d="M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5z"/></symbol>
33 +</svg>
34 +
18 35 <header class="top">
19 36 <a class="brand" href="/playground" data-link>
20 − <svg width="22" height="22" viewBox="0 0 32 32" aria-hidden="true"><path d="M3 9c6 0 8 6 13 6s7-6 13-6" fill="none" stroke="var(--accent)" stroke-width="2.4" stroke-linecap="round"/><path d="M3 16c6 0 8 6 13 6s7-6 13-6" fill="none" stroke="var(--accent)" stroke-width="2.4" stroke-linecap="round" opacity=".65"/><path d="M3 23c6 0 8 6 13 6s7-6 13-6" fill="none" stroke="var(--accent)" stroke-width="2.4" stroke-linecap="round" opacity=".35"/></svg>
37 + <svg width="24" height="24" viewBox="0 0 32 32" aria-hidden="true"><path d="M3 9c6 0 8 6 13 6s7-6 13-6" fill="none" stroke="var(--accent)" stroke-width="2.6" stroke-linecap="round"/><path d="M3 16c6 0 8 6 13 6s7-6 13-6" fill="none" stroke="var(--accent)" stroke-width="2.6" stroke-linecap="round" opacity=".65"/><path d="M3 23c6 0 8 6 13 6s7-6 13-6" fill="none" stroke="var(--accent)" stroke-width="2.6" stroke-linecap="round" opacity=".35"/></svg>
21 38 <span>Trawls</span><span class="ver" id="ver"></span>
22 39 </a>
23 40 <nav class="nav" id="nav">
@@ -30,7 +47,8 @@
30 47 </nav>
31 48 <div class="top-right">
32 49 <span class="status" id="status"><i></i><span>…</span></span>
33 − <button class="btn ghost sm" id="btn-key" title="Clé API">🔑</button>
50 + <button class="icon-btn" id="btn-theme" title="Thème clair / sombre" aria-label="Changer de thème"><svg class="sun"><use href="#i-sun"/></svg><svg class="moon"><use href="#i-moon"/></svg></button>
51 + <button class="icon-btn" id="btn-key" title="Clé API" aria-label="Clé API"><svg><use href="#i-key"/></svg></button>
34 52 </div>
35 53 </header>
36 54
@@ -41,10 +59,18 @@
41 59 <span class="mono" id="foot-info"></span>
42 60 </footer>
43 61
62 +<nav class="tabbar" id="tabbar" aria-label="Navigation">
63 + <a href="/playground" data-link data-route="playground"><svg><use href="#i-play"/></svg>Playground</a>
64 + <a href="/crawls" data-link data-route="crawls"><svg><use href="#i-crawl"/></svg>Crawls</a>
65 + <a href="/map" data-link data-route="map"><svg><use href="#i-map"/></svg>Map</a>
66 + <a href="/extract" data-link data-route="extract"><svg><use href="#i-extract"/></svg>Extract</a>
67 + <a href="/keys" data-link data-route="keys"><svg><use href="#i-key"/></svg>Keys</a>
68 +</nav>
69 +
44 70 <dialog id="dlg-key" class="dlg">
45 71 <form method="dialog">
46 72 <h3>Clé API</h3>
47 − <p class="muted">Stockée localement dans ce navigateur (chiffrée légèrement). Laisser vide en self-host sans auth.</p>
73 + <p class="muted">Stockée localement dans ce navigateur (obfusquée). Laisser vide en self-host sans auth.</p>
48 74 <input id="key-input" class="input mono" placeholder="trw_… ou clé admin" autocomplete="off">
49 75 <div class="row end gap">
50 76 <button class="btn ghost" value="cancel">Annuler</button>
@@ -60,10 +86,11 @@
60 86 <label class="field">
61 87 <span>URL</span>
62 88 <div class="row gap">
63 − <input class="input mono grow" id="pg-url" placeholder="https://exemple.com/article" required autocomplete="off" spellcheck="false">
89 + <input class="input mono grow" id="pg-url" type="url" inputmode="url" placeholder="https://exemple.com/article" required autocomplete="off" autocapitalize="off" spellcheck="false">
64 90 <button class="btn" id="pg-run" type="submit">Run <kbd>⌘↵</kbd></button>
65 91 </div>
66 92 </label>
93 + <details class="opts" id="pg-opts" open><summary>Options</summary><div class="stack">
67 94 <div class="grid2">
68 95 <label class="field"><span>Formats</span>
69 96 <div class="chips" id="pg-formats">
@@ -80,7 +107,7 @@
80 107 <select class="input" id="pg-mode"><option value="auto">auto (http → browser → stealth)</option><option value="http">http</option><option value="browser">browser</option><option value="stealth">stealth</option></select>
81 108 </label>
82 109 <label class="field"><span>wait_for (sélecteur ou ms)</span><input class="input mono" id="pg-wait" placeholder=".article-body ou 2000"></label>
83 − <label class="field"><span>timeout_ms</span><input class="input mono" id="pg-timeout" type="number" value="30000" min="1000" step="1000"></label>
110 + <label class="field"><span>timeout_ms</span><input class="input mono" id="pg-timeout" type="number" inputmode="numeric" value="30000" min="1000" step="1000"></label>
84 111 <label class="field"><span>exclude_tags (CSS, virgules)</span><input class="input mono" id="pg-exclude" placeholder=".ads, #comments"></label>
85 112 <label class="field"><span>include_tags</span><input class="input mono" id="pg-include" placeholder="article"></label>
86 113 </div>
@@ -102,6 +129,7 @@
102 129 llm: {"type":"object","properties":{"title":{"type":"string"},"price":{"type":"number"}}}'></textarea>
103 130 <input class="input" id="pg-ex-prompt" placeholder="prompt optionnel (llm)">
104 131 </details>
132 + </div></details>
105 133 </form>
106 134 <div class="hist">
107 135 <div class="row between"><h4>Historique</h4><button class="btn ghost sm" id="pg-hist-clear">vider</button></div>
@@ -123,10 +151,10 @@ llm: {"type":"object","properties":{"title":{"type":"string"},"price":{"type":"n
123 151 <button class="btn ghost sm" data-copy="curl">cURL</button>
124 152 <button class="btn ghost sm" data-copy="python">Python</button>
125 153 <button class="btn ghost sm" data-copy="ts">TS</button>
126 − <button class="btn ghost sm" data-copy="out">Copier sortie</button>
154 + <button class="btn ghost sm" data-copy="out">Copier</button>
127 155 </div>
128 156 </div>
129 − <div class="result-bar mono" id="pg-bar">Prêt. Entrez une URL.</div>
157 + <div class="result-bar" id="pg-bar">Prêt. Entrez une URL.</div>
130 158 <div class="result" id="pg-result"><div class="empty">Le résultat s'affiche ici.</div></div>
131 159 </div>
132 160 </section>
@@ -136,16 +164,17 @@ llm: {"type":"object","properties":{"title":{"type":"string"},"price":{"type":"n
136 164 <section class="page crawls">
137 165 <div class="panel left">
138 166 <form id="cr-form" class="stack">
139 − <label class="field"><span>URL racine</span><input class="input mono" id="cr-url" placeholder="https://docs.exemple.com" required></label>
167 + <label class="field"><span>URL racine</span><input class="input mono" id="cr-url" type="url" inputmode="url" placeholder="https://docs.exemple.com" required autocapitalize="off"></label>
168 + <details class="opts" id="cr-opts" open><summary>Options</summary><div class="stack">
140 169 <div class="grid2">
141 − <label class="field"><span>max_pages</span><input class="input mono" id="cr-pages" type="number" value="50" min="1" max="20000"></label>
142 − <label class="field"><span>max_depth</span><input class="input mono" id="cr-depth" type="number" value="3" min="0" max="20"></label>
143 − <label class="field"><span>concurrency</span><input class="input mono" id="cr-conc" type="number" value="5" min="1" max="32"></label>
170 + <label class="field"><span>max_pages</span><input class="input mono" id="cr-pages" type="number" inputmode="numeric" value="50" min="1" max="20000"></label>
171 + <label class="field"><span>max_depth</span><input class="input mono" id="cr-depth" type="number" inputmode="numeric" value="3" min="0" max="20"></label>
172 + <label class="field"><span>concurrency</span><input class="input mono" id="cr-conc" type="number" inputmode="numeric" value="5" min="1" max="32"></label>
144 173 <label class="field"><span>strategy</span><select class="input" id="cr-strat"><option>bfs</option><option>dfs</option><option>best_first</option></select></label>
145 174 <label class="field"><span>include_paths (globs, virgules)</span><input class="input mono" id="cr-inc" placeholder="/docs/*, /blog/*"></label>
146 175 <label class="field"><span>exclude_paths</span><input class="input mono" id="cr-exc" placeholder="/tag/*, *.pdf"></label>
147 176 <label class="field"><span>search (best_first)</span><input class="input" id="cr-search" placeholder="pricing api"></label>
148 − <label class="field"><span>delay_ms</span><input class="input mono" id="cr-delay" type="number" value="0" min="0"></label>
177 + <label class="field"><span>delay_ms</span><input class="input mono" id="cr-delay" type="number" inputmode="numeric" value="0" min="0"></label>
149 178 </div>
150 179 <div class="row gap wrap">
151 180 <label class="check"><input type="checkbox" id="cr-sub"> allow_subdomains</label>
@@ -155,10 +184,11 @@ llm: {"type":"object","properties":{"title":{"type":"string"},"price":{"type":"n
155 184 <label class="check"><input type="checkbox" id="cr-links"> links dans la sortie</label>
156 185 </div>
157 186 <details class="adv"><summary>Requête JSON brute (écrase le formulaire)</summary><textarea class="input mono" id="cr-json" rows="6" placeholder='{"url":"https://…","crawl":{"max_pages":100},"scrape":{"formats":["markdown"]}}'></textarea></details>
187 + </div></details>
158 188 <div class="row gap"><button class="btn" type="submit">Lancer le crawl</button></div>
159 189 </form>
160 190 <div class="row between"><h4>Jobs</h4><button class="btn ghost sm" id="cr-refresh">↻</button></div>
161 − <table class="tbl" id="cr-jobs"><thead><tr><th>id</th><th>type</th><th>statut</th><th>pages</th><th>durée</th><th></th></tr></thead><tbody></tbody></table>
191 + <div class="tbl-wrap"><table class="tbl" id="cr-jobs"><thead><tr><th>id</th><th>type</th><th>statut</th><th>pages</th><th>durée</th><th></th></tr></thead><tbody></tbody></table></div>
162 192 </div>
163 193 <div class="panel right" id="cr-detail">
164 194 <div class="empty">Sélectionnez un job ou lancez un crawl.</div>
@@ -169,14 +199,14 @@ llm: {"type":"object","properties":{"title":{"type":"string"},"price":{"type":"n
169 199 <template id="tpl-map">
170 200 <section class="page mapp">
171 201 <form id="mp-form" class="row gap wrap top-form">
172 − <input class="input mono grow" id="mp-url" placeholder="https://exemple.com" required>
202 + <input class="input mono grow" id="mp-url" type="url" inputmode="url" placeholder="https://exemple.com" required autocapitalize="off">
173 203 <input class="input" id="mp-search" placeholder="search (BM25, optionnel)">
174 − <input class="input mono" id="mp-limit" type="number" value="2000" min="1" max="20000" style="width:8rem" title="limit">
204 + <input class="input mono" id="mp-limit" type="number" inputmode="numeric" value="2000" min="1" max="20000" style="width:8rem" title="limit">
175 205 <label class="check"><input type="checkbox" id="mp-sub"> sous-domaines</label>
176 206 <label class="check"><input type="checkbox" id="mp-titles"> titres</label>
177 207 <button class="btn" type="submit">Map</button>
178 208 </form>
179 − <div class="result-bar mono" id="mp-bar">—</div>
209 + <div class="result-bar" id="mp-bar">—</div>
180 210 <div class="row gap wrap tools">
181 211 <input class="input mono grow" id="mp-filter" placeholder="filtre instantané…">
182 212 <label class="check"><input type="checkbox" id="mp-group"> grouper par path</label>
@@ -193,7 +223,7 @@ llm: {"type":"object","properties":{"title":{"type":"string"},"price":{"type":"n
193 223 <section class="page extract">
194 224 <div class="panel left">
195 225 <form id="ex-form" class="stack">
196 − <label class="field"><span>URLs (une par ligne) ou pattern (https://site.com/produits/*)</span><textarea class="input mono" id="ex-urls" rows="4" required></textarea></label>
226 + <label class="field"><span>URLs (une par ligne) ou pattern (https://site.com/produits/*)</span><textarea class="input mono" id="ex-urls" rows="4" required autocapitalize="off"></textarea></label>
197 227 <div class="row gap wrap">
198 228 <label class="check"><input type="radio" name="ex-mode" value="css" checked> CSS (déterministe)</label>
199 229 <label class="check"><input type="radio" name="ex-mode" value="llm"> LLM (schéma JSON)</label>
@@ -207,7 +237,7 @@ llm: {"type":"object","properties":{"title":{"type":"string"},"price":{"type":"n
207 237 </form>
208 238 </div>
209 239 <div class="panel right">
210 − <div class="result-bar mono" id="ex-bar">—</div>
240 + <div class="result-bar" id="ex-bar">—</div>
211 241 <div class="result" id="ex-result"><div class="empty">Résultat par URL et fusion.</div></div>
212 242 </div>
213 243 </section>
@@ -223,12 +253,12 @@ llm: {"type":"object","properties":{"title":{"type":"string"},"price":{"type":"n
223 253 </div>
224 254 <div class="panel right">
225 255 <div class="row between"><h4>Clés API</h4>
226 − <form id="ky-form" class="row gap"><input class="input" id="ky-name" placeholder="nom" required><input class="input mono" id="ky-quota" type="number" placeholder="quota/jour" style="width:8rem"><button class="btn sm" type="submit">Créer</button></form>
256 + <form id="ky-form" class="row gap"><input class="input" id="ky-name" placeholder="nom" required><input class="input mono" id="ky-quota" type="number" inputmode="numeric" placeholder="quota/jour" style="width:8rem"><button class="btn sm" type="submit">Créer</button></form>
227 257 </div>
228 258 <div id="ky-new" class="callout hidden"></div>
229 − <table class="tbl" id="ky-table"><thead><tr><th>id</th><th>nom</th><th>préfixe</th><th>quota</th><th>dernier usage</th><th></th></tr></thead><tbody></tbody></table>
259 + <div class="tbl-wrap"><table class="tbl" id="ky-table"><thead><tr><th>id</th><th>nom</th><th>préfixe</th><th>quota</th><th>dernier usage</th><th></th></tr></thead><tbody></tbody></table></div>
230 260 <h4>Dernières requêtes</h4>
231 − <table class="tbl mono sm" id="ky-req"><thead><tr><th>quand</th><th>méthode</th><th>path</th><th>statut</th><th>ms</th></tr></thead><tbody></tbody></table>
261 + <div class="tbl-wrap"><table class="tbl mono sm" id="ky-req"><thead><tr><th>quand</th><th>méthode</th><th>path</th><th>statut</th><th>ms</th></tr></thead><tbody></tbody></table></div>
232 262 </div>
233 263 </section>
234 264 </template>
modified trawls/web/static/style.css +199 −86
@@ -1,125 +1,238 @@
1 +/* Trawls UI — thème clair par défaut, sombre en option ; mobile-first. */
1 2 :root{
2 − --bg:#0b0c0e;--bg2:#111317;--bg3:#171a1f;--line:#23272e;--line2:#2e333b;
3 + --bg:#f6f7f9;--bg2:#ffffff;--bg3:#eef0f3;--bg4:#e4e7eb;--line:#e3e6ea;--line2:#cfd4da;
4 + --fg:#111418;--fg2:#4b5563;--fg3:#8a929c;
5 + --accent:#d97706;--accent2:#b45309;--accent-soft:rgba(217,119,6,.10);--accent-fg:#ffffff;
6 + --ok:#15803d;--warn:#b45309;--err:#dc2626;--info:#2563eb;
7 + --ok-soft:rgba(21,128,61,.10);--err-soft:rgba(220,38,38,.10);--warn-soft:rgba(180,83,9,.10);--info-soft:rgba(37,99,235,.10);
8 + --shadow:0 1px 2px rgba(16,24,40,.06),0 1px 3px rgba(16,24,40,.08);--shadow-lg:0 10px 30px rgba(16,24,40,.14);
9 + --mono:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,monospace;--sans:Inter,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
10 + --r:8px;--r-lg:12px;--top:52px;--foot:30px;--tabbar:58px;
11 + --sat:env(safe-area-inset-top,0px);--sab:env(safe-area-inset-bottom,0px);
12 + --json-k:#1d4ed8;--json-s:#047857;--json-n:#b45309;--json-b:#be123c;
13 + color-scheme:light;
14 +}
15 +html[data-theme="dark"]{
16 + --bg:#0b0c0e;--bg2:#111317;--bg3:#171a1f;--bg4:#1f242b;--line:#23272e;--line2:#2e333b;
3 17 --fg:#e6e8eb;--fg2:#a2a8b3;--fg3:#6b7280;
4 − --accent:#f5a524;--accent2:#c98416;--ok:#3fb950;--warn:#d29922;--err:#f85149;--info:#58a6ff;
5 − --mono:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,monospace;--sans:Inter,system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
6 − --r:6px;--top:44px;--foot:28px;
18 + --accent:#f5a524;--accent2:#c98416;--accent-soft:rgba(245,165,36,.12);--accent-fg:#161000;
19 + --ok:#3fb950;--warn:#d29922;--err:#f85149;--info:#58a6ff;
20 + --ok-soft:rgba(63,185,80,.14);--err-soft:rgba(248,81,73,.14);--warn-soft:rgba(210,153,34,.14);--info-soft:rgba(88,166,255,.14);
21 + --shadow:none;--shadow-lg:0 10px 30px rgba(0,0,0,.5);
22 + --json-k:#79c0ff;--json-s:#a5d6ff;--json-n:#f5a524;--json-b:#ff7b72;
23 + color-scheme:dark;
7 24 }
8 25 *{box-sizing:border-box}
26 +html{-webkit-text-size-adjust:100%}
9 27 html,body{height:100%}
10 −body{margin:0;background:var(--bg);color:var(--fg);font:13px/1.45 var(--sans);-webkit-font-smoothing:antialiased;display:flex;flex-direction:column;min-height:100dvh}
11 −a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}
12 −.mono,code,pre,kbd,.input.mono,textarea.mono{font-family:var(--mono);font-size:12px}
28 +body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.5 var(--sans);-webkit-font-smoothing:antialiased;display:flex;flex-direction:column;min-height:100dvh;padding-top:var(--sat)}
29 +a{color:var(--accent2);text-decoration:none}a:hover{text-decoration:underline}
30 +html[data-theme="dark"] a{color:var(--accent)}
31 +.mono,code,pre,kbd,.input.mono,textarea.mono{font-family:var(--mono);font-size:13px}
13 32 .muted{color:var(--fg2)}.hidden{display:none!important}.grow{flex:1;min-width:0}
14 33 .row{display:flex;align-items:center}.row.gap{gap:8px}.row.between{justify-content:space-between}.row.end{justify-content:flex-end}.row.wrap{flex-wrap:wrap}
15 −.stack{display:flex;flex-direction:column;gap:10px}.stack.sm{gap:6px}
16 −.grid2{display:grid;grid-template-columns:1fr 1fr;gap:8px 12px}
17 −h3,h4{margin:6px 0;font-weight:600}h4{font-size:12px;color:var(--fg2);text-transform:uppercase;letter-spacing:.04em}
18 −kbd{background:var(--bg3);border:1px solid var(--line2);border-radius:3px;padding:0 4px;font-size:10px;color:var(--fg2);margin-left:6px}
34 +.stack{display:flex;flex-direction:column;gap:12px}.stack.sm{gap:8px}
35 +.grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px 12px}
36 +h3,h4{margin:8px 0;font-weight:600}h3{font-size:16px}h4{font-size:11px;color:var(--fg2);text-transform:uppercase;letter-spacing:.06em}
37 +kbd{background:var(--bg3);border:1px solid var(--line2);border-radius:4px;padding:0 5px;font-size:10px;color:var(--fg2);margin-left:6px}
38 +button,input,select,textarea{font-family:inherit}
39 +:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
19 40
20 −.top{position:sticky;top:0;z-index:10;height:var(--top);display:flex;align-items:center;gap:18px;padding:0 14px;background:var(--bg2);border-bottom:1px solid var(--line)}
21 −.brand{display:flex;align-items:center;gap:8px;font-weight:700;font-size:14px;color:var(--fg)}.brand:hover{text-decoration:none}
41 +/* ---- top bar ---- */
42 +.top{position:sticky;top:0;z-index:20;height:var(--top);display:flex;align-items:center;gap:16px;padding:0 max(14px,env(safe-area-inset-left)) 0 max(14px,env(safe-area-inset-right));background:color-mix(in srgb,var(--bg2) 88%,transparent);backdrop-filter:saturate(1.4) blur(10px);-webkit-backdrop-filter:saturate(1.4) blur(10px);border-bottom:1px solid var(--line)}
43 +.brand{display:flex;align-items:center;gap:8px;font-weight:700;font-size:15px;color:var(--fg);letter-spacing:-.01em}.brand:hover{text-decoration:none}
22 44 .brand .ver{font-family:var(--mono);font-size:10px;color:var(--fg3);font-weight:400}
23 45 .nav{display:flex;gap:2px;flex:1}
24 −.nav a{padding:5px 10px;border-radius:var(--r);color:var(--fg2);font-weight:500}
25 −.nav a:hover{background:var(--bg3);text-decoration:none;color:var(--fg)}.nav a.on{color:var(--accent);background:rgba(245,165,36,.08)}
46 +.nav a{padding:6px 11px;border-radius:var(--r);color:var(--fg2);font-weight:500;font-size:13px;display:inline-flex;align-items:center;gap:6px}
47 +.nav a svg{width:15px;height:15px;display:none}
48 +.nav a:hover{background:var(--bg3);text-decoration:none;color:var(--fg)}.nav a.on{color:var(--accent2);background:var(--accent-soft)}
49 +html[data-theme="dark"] .nav a.on{color:var(--accent)}
26 50 .top-right{display:flex;align-items:center;gap:8px}
27 51 .status{display:flex;align-items:center;gap:6px;font-family:var(--mono);font-size:11px;color:var(--fg2)}
28 −.status i{width:8px;height:8px;border-radius:50%;background:var(--fg3);display:inline-block}
52 +.status i{width:8px;height:8px;border-radius:50%;background:var(--fg3);display:inline-block;box-shadow:0 0 0 3px var(--bg3)}
29 53 .status.ok i{background:var(--ok)}.status.err i{background:var(--err)}.status.warn i{background:var(--warn)}
54 +.icon-btn{width:34px;height:34px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--line2);background:var(--bg2);color:var(--fg2);border-radius:var(--r);cursor:pointer;padding:0}
55 +.icon-btn:hover{color:var(--fg);background:var(--bg3)}.icon-btn svg{width:16px;height:16px}
56 +.icon-btn .sun{display:none}.icon-btn .moon{display:block}
57 +html[data-theme="dark"] .icon-btn .sun{display:block}html[data-theme="dark"] .icon-btn .moon{display:none}
30 58
59 +/* ---- layout ---- */
31 60 .view{flex:1;display:flex;min-height:0}
32 −.page{flex:1;display:grid;grid-template-columns:minmax(340px,420px) 1fr;min-height:calc(100dvh - var(--top) - var(--foot))}
33 −.page.mapp,.page.keys{grid-template-columns:1fr}
61 +.page{flex:1;min-width:0;display:grid;grid-template-columns:minmax(340px,420px) 1fr;min-height:calc(100dvh - var(--top) - var(--foot) - var(--sat))}
62 +details.opts{border:1px solid var(--line);border-radius:var(--r);padding:8px 10px;background:var(--bg)}
63 +details.opts>summary{cursor:pointer;color:var(--fg2);font-size:13px;font-weight:500;min-height:28px;display:flex;align-items:center;list-style:none}
64 +details.opts>summary::-webkit-details-marker{display:none}
65 +details.opts>summary::before{content:"▸";margin-right:8px;color:var(--fg3);transition:transform .15s}
66 +details.opts[open]>summary::before{transform:rotate(90deg)}
67 +details.opts[open]>summary{margin-bottom:10px}
68 +details.opts>.stack{gap:12px}
69 +.page.mapp{grid-template-columns:1fr;display:flex;flex-direction:column}
34 70 .page.keys{grid-template-columns:minmax(320px,400px) 1fr}
35 −.panel{padding:14px;overflow:auto;min-width:0}
71 +.panel{padding:16px;overflow:auto;min-width:0;-webkit-overflow-scrolling:touch}
36 72 .panel.left{border-right:1px solid var(--line);background:var(--bg2)}
37 73 .panel.right{display:flex;flex-direction:column;min-height:0}
38 74
39 −.field{display:flex;flex-direction:column;gap:4px}
40 −.field>span{font-size:11px;color:var(--fg2);font-weight:500}
41 −.input{background:var(--bg);border:1px solid var(--line2);color:var(--fg);border-radius:var(--r);padding:6px 8px;font:inherit;outline:none;min-width:0;width:100%}
42 −.input:focus{border-color:var(--accent);box-shadow:0 0 0 2px rgba(245,165,36,.18)}
43 −textarea.input{resize:vertical;line-height:1.4}
44 −select.input{appearance:none;background-image:linear-gradient(45deg,transparent 50%,var(--fg2) 50%),linear-gradient(135deg,var(--fg2) 50%,transparent 50%);background-position:calc(100% - 14px) 50%,calc(100% - 9px) 50%;background-size:5px 5px;background-repeat:no-repeat;padding-right:26px}
45 −.btn{background:var(--accent);color:#161000;border:1px solid var(--accent2);border-radius:var(--r);padding:6px 12px;font:inherit;font-weight:600;cursor:pointer;white-space:nowrap}
46 −.btn:hover{background:#ffb640}.btn:disabled{opacity:.5;cursor:progress}
47 −.btn.ghost{background:transparent;color:var(--fg2);border-color:var(--line2)}.btn.ghost:hover{color:var(--fg);background:var(--bg3)}
48 −.btn.sm{padding:3px 8px;font-size:11px}
49 −.btn.danger{background:transparent;color:var(--err);border-color:var(--line2)}
50 −.check{display:flex;align-items:center;gap:5px;color:var(--fg2);font-size:12px;cursor:pointer;user-select:none}
51 −.check input{accent-color:var(--accent)}
52 −.chips{display:flex;flex-wrap:wrap;gap:4px}
53 −.chips label{display:flex;align-items:center;gap:4px;border:1px solid var(--line2);border-radius:999px;padding:2px 8px;font-size:11px;color:var(--fg2);cursor:pointer;font-family:var(--mono)}
54 −.chips label:has(input:checked){border-color:var(--accent);color:var(--accent);background:rgba(245,165,36,.08)}
75 +/* ---- forms ---- */
76 +.field{display:flex;flex-direction:column;gap:5px}
77 +.field>span{font-size:12px;color:var(--fg2);font-weight:500}
78 +.input{background:var(--bg2);border:1px solid var(--line2);color:var(--fg);border-radius:var(--r);padding:9px 11px;font:inherit;font-size:14px;outline:none;min-width:0;width:100%;min-height:40px;box-shadow:var(--shadow)}
79 +html[data-theme="dark"] .input{background:var(--bg)}
80 +.input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
81 +textarea.input{resize:vertical;line-height:1.45;min-height:64px}
82 +select.input{appearance:none;background-image:linear-gradient(45deg,transparent 50%,var(--fg2) 50%),linear-gradient(135deg,var(--fg2) 50%,transparent 50%);background-position:calc(100% - 15px) 50%,calc(100% - 10px) 50%;background-size:5px 5px;background-repeat:no-repeat;padding-right:28px}
83 +.btn{background:var(--accent);color:var(--accent-fg);border:1px solid var(--accent2);border-radius:var(--r);padding:9px 14px;font:inherit;font-size:14px;font-weight:600;cursor:pointer;white-space:nowrap;min-height:40px;display:inline-flex;align-items:center;justify-content:center;gap:6px;box-shadow:var(--shadow)}
84 +.btn:hover{filter:brightness(1.06)}.btn:disabled{opacity:.55;cursor:progress}
85 +.btn.ghost{background:var(--bg2);color:var(--fg2);border-color:var(--line2)}.btn.ghost:hover{color:var(--fg);background:var(--bg3);filter:none}
86 +html[data-theme="dark"] .btn.ghost{background:transparent}
87 +.btn.sm{padding:5px 10px;font-size:12px;min-height:32px}
88 +.btn.danger{background:var(--bg2);color:var(--err);border-color:var(--line2)}
89 +.check{display:flex;align-items:center;gap:6px;color:var(--fg2);font-size:13px;cursor:pointer;user-select:none;min-height:32px}
90 +.check input{accent-color:var(--accent);width:16px;height:16px}
91 +.chips{display:flex;flex-wrap:wrap;gap:6px}
92 +.chips label{display:flex;align-items:center;gap:4px;border:1px solid var(--line2);border-radius:999px;padding:5px 11px;font-size:12px;color:var(--fg2);cursor:pointer;font-family:var(--mono);background:var(--bg2);min-height:30px}
93 +.chips label:has(input:checked){border-color:var(--accent);color:var(--accent2);background:var(--accent-soft)}
94 +html[data-theme="dark"] .chips label:has(input:checked){color:var(--accent)}
55 95 .chips input{display:none}
56 −details.adv{border:1px solid var(--line);border-radius:var(--r);padding:6px 8px;background:var(--bg)}
57 −details.adv summary{cursor:pointer;color:var(--fg2);font-size:12px;font-weight:500}
96 +details.adv{border:1px solid var(--line);border-radius:var(--r);padding:8px 10px;background:var(--bg)}
97 +html[data-theme="dark"] details.adv{background:var(--bg)}
98 +details.adv summary{cursor:pointer;color:var(--fg2);font-size:13px;font-weight:500;min-height:28px;display:flex;align-items:center}
58 99 details.adv[open] summary{margin-bottom:8px}
59 100 details.adv .input{margin-top:6px}
60 101
61 −.tabs{display:flex;align-items:center;gap:2px;border-bottom:1px solid var(--line);padding:0 0 6px;flex-wrap:wrap}
62 −.tabs>button{background:none;border:0;color:var(--fg2);padding:5px 10px;border-radius:var(--r);cursor:pointer;font:inherit;font-weight:500}
63 −.tabs>button:hover{color:var(--fg);background:var(--bg3)}.tabs>button.on{color:var(--accent);background:rgba(245,165,36,.08)}
64 −.copy-group{display:flex;gap:4px}
65 −.result-bar{padding:6px 0;color:var(--fg2);font-size:11px;border-bottom:1px solid var(--line);display:flex;gap:12px;flex-wrap:wrap}
102 +/* ---- tabs & results ---- */
103 +.tabs{display:flex;align-items:center;gap:2px;border-bottom:1px solid var(--line);padding:0 0 8px;overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch}
104 +.tabs::-webkit-scrollbar{display:none}
105 +.tabs>button{background:none;border:0;color:var(--fg2);padding:7px 11px;border-radius:var(--r);cursor:pointer;font:inherit;font-size:13px;font-weight:500;white-space:nowrap;flex:0 0 auto}
106 +.tabs>button:hover{color:var(--fg);background:var(--bg3)}.tabs>button.on{color:var(--accent2);background:var(--accent-soft)}
107 +html[data-theme="dark"] .tabs>button.on{color:var(--accent)}
108 +.copy-group{display:flex;gap:4px;flex:0 0 auto}
109 +.result-bar{padding:8px 0;color:var(--fg2);font-size:12px;border-bottom:1px solid var(--line);display:flex;gap:12px;flex-wrap:wrap;font-family:var(--mono)}
66 110 .result-bar .ok{color:var(--ok)}.result-bar .err{color:var(--err)}.result-bar .warn{color:var(--warn)}.result-bar b{color:var(--fg)}
67 −.result{flex:1;overflow:auto;padding:12px 4px;min-height:0}
68 −.empty{color:var(--fg3);padding:40px;text-align:center}
69 −.prose{max-width:860px;line-height:1.6;font-size:14px}
70 −.prose h1{font-size:22px;margin:.6em 0 .4em}.prose h2{font-size:18px;margin:1em 0 .4em;border-bottom:1px solid var(--line);padding-bottom:4px}.prose h3{font-size:15px}
71 −.prose pre{background:var(--bg2);border:1px solid var(--line);padding:10px;border-radius:var(--r);overflow:auto}.prose code{background:var(--bg3);padding:1px 4px;border-radius:3px}.prose pre code{background:none;padding:0}
72 −.prose table{border-collapse:collapse;font-size:13px}.prose td,.prose th{border:1px solid var(--line2);padding:4px 8px}.prose th{background:var(--bg3)}
73 −.prose img{max-width:100%;border-radius:var(--r)}.prose blockquote{border-left:3px solid var(--accent);margin:0;padding:2px 12px;color:var(--fg2)}
74 −pre.code{background:var(--bg2);border:1px solid var(--line);border-radius:var(--r);padding:10px;overflow:auto;white-space:pre-wrap;word-break:break-word;margin:0;font-size:12px;line-height:1.45}
75 −.json-k{color:#79c0ff}.json-s{color:#a5d6ff}.json-n{color:#f5a524}.json-b{color:#ff7b72}.json-null{color:var(--fg3)}
111 +.result{flex:1;overflow:auto;padding:14px 4px;min-height:0;-webkit-overflow-scrolling:touch}
112 +.empty{color:var(--fg3);padding:48px 20px;text-align:center}
113 +.prose{max-width:860px;line-height:1.65;font-size:15px;overflow-wrap:anywhere}
114 +.prose h1{font-size:24px;margin:.5em 0 .4em;letter-spacing:-.01em}.prose h2{font-size:19px;margin:1em 0 .4em;border-bottom:1px solid var(--line);padding-bottom:4px}.prose h3{font-size:16px}
115 +.prose pre{background:var(--bg3);border:1px solid var(--line);padding:12px;border-radius:var(--r);overflow:auto}.prose code{background:var(--bg3);padding:1px 5px;border-radius:4px}.prose pre code{background:none;padding:0}
116 +.prose table{border-collapse:collapse;font-size:13px;display:block;overflow-x:auto;max-width:100%}.prose td,.prose th{border:1px solid var(--line2);padding:5px 9px}.prose th{background:var(--bg3)}
117 +.prose img{max-width:100%;border-radius:var(--r)}.prose blockquote{border-left:3px solid var(--accent);margin:0;padding:2px 14px;color:var(--fg2)}
118 +pre.code{background:var(--bg3);border:1px solid var(--line);border-radius:var(--r);padding:12px;overflow:auto;white-space:pre-wrap;word-break:break-word;margin:0;font-size:12.5px;line-height:1.5}
119 +.json-k{color:var(--json-k)}.json-s{color:var(--json-s)}.json-n{color:var(--json-n)}.json-b{color:var(--json-b)}.json-null{color:var(--fg3)}
76 120
77 −.tbl{width:100%;border-collapse:collapse;font-size:12px}
78 −.tbl th{text-align:left;color:var(--fg2);font-weight:500;padding:6px 8px;border-bottom:1px solid var(--line);font-size:11px;text-transform:uppercase;letter-spacing:.03em}
79 −.tbl td{padding:5px 8px;border-bottom:1px solid var(--line);vertical-align:top}
80 −.tbl tr:hover td{background:var(--bg3)}.tbl tr.sel td{background:rgba(245,165,36,.07)}
81 −.tbl.sm td,.tbl.sm th{padding:3px 6px;font-size:11px}
82 −.badge{display:inline-block;padding:1px 6px;border-radius:999px;font-size:10px;font-family:var(--mono);border:1px solid var(--line2);color:var(--fg2)}
83 −.badge.ok{color:var(--ok);border-color:rgba(63,185,80,.4)}.badge.failed,.badge.err{color:var(--err);border-color:rgba(248,81,73,.4)}
84 −.badge.running,.badge.queued{color:var(--accent);border-color:rgba(245,165,36,.4)}.badge.skipped{color:var(--warn)}.badge.completed{color:var(--info);border-color:rgba(88,166,255,.4)}.badge.cancelled{color:var(--fg3)}
85 −.link-btn{background:none;border:0;color:var(--accent);cursor:pointer;font:inherit;padding:0}
121 +/* ---- tables ---- */
122 +.tbl-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;border:1px solid var(--line);border-radius:var(--r);background:var(--bg2)}
123 +.tbl{width:100%;border-collapse:collapse;font-size:13px;min-width:520px}
124 +.tbl th{text-align:left;color:var(--fg2);font-weight:500;padding:8px 10px;border-bottom:1px solid var(--line);font-size:11px;text-transform:uppercase;letter-spacing:.04em;background:var(--bg3);position:sticky;top:0}
125 +.tbl td{padding:8px 10px;border-bottom:1px solid var(--line);vertical-align:top}
126 +.tbl tr:last-child td{border-bottom:0}
127 +.tbl tbody tr{cursor:pointer}.tbl tr:hover td{background:var(--bg3)}.tbl tr.sel td{background:var(--accent-soft)}
128 +.tbl.sm td,.tbl.sm th{padding:5px 8px;font-size:12px}
129 +.badge{display:inline-block;padding:2px 8px;border-radius:999px;font-size:11px;font-family:var(--mono);border:1px solid var(--line2);color:var(--fg2);background:var(--bg2);line-height:1.4}
130 +.badge.ok{color:var(--ok);border-color:transparent;background:var(--ok-soft)}.badge.failed,.badge.err{color:var(--err);border-color:transparent;background:var(--err-soft)}
131 +.badge.running,.badge.queued{color:var(--accent2);border-color:transparent;background:var(--accent-soft)}.badge.skipped{color:var(--warn);border-color:transparent;background:var(--warn-soft)}.badge.completed{color:var(--info);border-color:transparent;background:var(--info-soft)}.badge.cancelled{color:var(--fg3)}
132 +html[data-theme="dark"] .badge.running,html[data-theme="dark"] .badge.queued{color:var(--accent)}
133 +.link-btn{background:none;border:0;color:var(--accent2);cursor:pointer;font:inherit;padding:6px;min-width:32px;min-height:32px}
134 +html[data-theme="dark"] .link-btn{color:var(--accent)}
86 135
87 136 .list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px}
88 −.list li{display:flex;gap:8px;align-items:center;padding:4px 6px;border-radius:var(--r);cursor:pointer;color:var(--fg2);font-family:var(--mono);font-size:11px;overflow:hidden}
137 +.list li{display:flex;gap:8px;align-items:center;padding:7px 8px;border-radius:var(--r);cursor:pointer;color:var(--fg2);font-family:var(--mono);font-size:12px;overflow:hidden;min-height:36px}
89 138 .list li:hover{background:var(--bg3);color:var(--fg)}.list li span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
90 −.hist{margin-top:16px}
139 +.hist{margin-top:18px}
91 140
92 −.progress{height:6px;background:var(--bg3);border-radius:3px;overflow:hidden;margin:6px 0}
141 +.progress{height:6px;background:var(--bg4);border-radius:3px;overflow:hidden;margin:8px 0}
93 142 .progress>i{display:block;height:100%;background:var(--accent);transition:width .3s}
94 −.tree{font-family:var(--mono);font-size:11px;line-height:1.7}
143 +.tree{font-family:var(--mono);font-size:12px;line-height:1.9}
95 144 .tree details{margin-left:10px}.tree summary{cursor:pointer;color:var(--fg2)}.tree summary:hover{color:var(--fg)}
96 −.tree .leaf{margin-left:24px;display:flex;gap:6px;align-items:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}
97 −.tree .leaf:hover{color:var(--accent)}
98 −.split{display:grid;grid-template-columns:minmax(260px,38%) 1fr;gap:12px;flex:1;min-height:0}
145 +.tree .leaf{margin-left:24px;display:flex;gap:6px;align-items:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer;min-height:30px}
146 +.tree .leaf:hover{color:var(--accent2)}
147 +.split{display:grid;grid-template-columns:minmax(260px,38%) 1fr;gap:14px;flex:1;min-height:0}
99 148 .split>div{min-height:0;overflow:auto}
100 −.log{font-family:var(--mono);font-size:11px;line-height:1.6;white-space:pre-wrap}
149 +.log{font-family:var(--mono);font-size:12px;line-height:1.7;white-space:pre-wrap;overflow-wrap:anywhere}
101 150 .log .ok{color:var(--ok)}.log .failed{color:var(--err)}.log .skipped{color:var(--warn)}
102 151
103 −.top-form{padding:12px 14px;border-bottom:1px solid var(--line);background:var(--bg2)}
104 −.tools{padding:8px 14px;border-bottom:1px solid var(--line)}
105 −.mapp .result-bar{padding:6px 14px}
106 −.vlist{flex:1;overflow:auto;font-family:var(--mono);font-size:11px;padding:4px 14px}
107 −.vrow{display:grid;grid-template-columns:20px 1fr 90px 70px;gap:8px;align-items:center;padding:3px 4px;border-bottom:1px solid var(--line);height:26px;white-space:nowrap;overflow:hidden}
152 +/* ---- map ---- */
153 +.top-form{padding:12px 16px;border-bottom:1px solid var(--line);background:var(--bg2)}
154 +.tools{padding:10px 16px;border-bottom:1px solid var(--line)}
155 +.mapp .result-bar{padding:8px 16px}
156 +.vlist{flex:1;overflow:auto;font-family:var(--mono);font-size:12px;padding:4px 16px;-webkit-overflow-scrolling:touch}
157 +.vrow{display:grid;grid-template-columns:22px 1fr 90px 70px;gap:8px;align-items:center;padding:4px;border-bottom:1px solid var(--line);min-height:34px;white-space:nowrap;overflow:hidden}
108 158 .vrow:hover{background:var(--bg3)}.vrow span{overflow:hidden;text-overflow:ellipsis}.vrow .src{color:var(--fg3)}
109 −.vgroup{padding:6px 4px 2px;color:var(--accent);font-weight:600;border-bottom:1px solid var(--line2)}
159 +.vrow input{width:16px;height:16px;accent-color:var(--accent)}
160 +.vgroup{padding:8px 4px 3px;color:var(--accent2);font-weight:600;border-bottom:1px solid var(--line2)}
161 +html[data-theme="dark"] .vgroup{color:var(--accent)}
110 162
111 −.ex-field{display:grid;grid-template-columns:1fr 1.4fr 90px 24px;gap:6px;align-items:center}
112 −.ex-field.llm{grid-template-columns:1fr 100px 1.4fr 24px}
113 −.callout{background:rgba(245,165,36,.08);border:1px solid var(--accent2);border-radius:var(--r);padding:10px;margin:8px 0;font-family:var(--mono);font-size:12px;word-break:break-all}
163 +/* ---- extract / keys ---- */
164 +.ex-field{display:grid;grid-template-columns:1fr 1.4fr 90px 32px;gap:6px;align-items:center;padding:8px;border:1px solid var(--line);border-radius:var(--r);background:var(--bg)}
165 +.ex-field.llm{grid-template-columns:1fr 100px 1.4fr 32px}
166 +.callout{background:var(--accent-soft);border:1px solid var(--accent2);border-radius:var(--r);padding:12px;margin:10px 0;font-family:var(--mono);font-size:12px;word-break:break-all}
114 167 .bars{display:flex;align-items:flex-end;gap:2px;height:80px;border-bottom:1px solid var(--line);padding:0 2px}
115 −.bars>i{flex:1;background:var(--accent);opacity:.8;border-radius:2px 2px 0 0;min-height:1px}
168 +.bars>i{flex:1;background:var(--accent);opacity:.85;border-radius:2px 2px 0 0;min-height:1px}
116 169 .bars>i:hover{opacity:1}
117 −.foot{height:var(--foot);display:flex;align-items:center;justify-content:space-between;padding:0 14px;border-top:1px solid var(--line);color:var(--fg3);font-size:11px;background:var(--bg2)}
118 −.dlg{background:var(--bg2);color:var(--fg);border:1px solid var(--line2);border-radius:8px;padding:16px;width:min(460px,92vw)}
119 −.dlg::backdrop{background:rgba(0,0,0,.6)}.dlg form{display:flex;flex-direction:column;gap:10px}
170 +.foot{height:var(--foot);display:flex;align-items:center;justify-content:space-between;padding:0 16px;border-top:1px solid var(--line);color:var(--fg3);font-size:11px;background:var(--bg2)}
171 +.dlg{background:var(--bg2);color:var(--fg);border:1px solid var(--line2);border-radius:var(--r-lg);padding:18px;width:min(460px,92vw);box-shadow:var(--shadow-lg)}
172 +.dlg::backdrop{background:rgba(0,0,0,.45)}.dlg form{display:flex;flex-direction:column;gap:12px}
120 173 .shot{max-width:100%;border:1px solid var(--line2);border-radius:var(--r)}
121 −.chunk{border:1px solid var(--line);border-radius:var(--r);padding:8px;margin-bottom:8px;background:var(--bg2)}
122 −.chunk .h{font-family:var(--mono);font-size:10px;color:var(--fg3);margin-bottom:4px;display:flex;gap:10px}
123 −.toast{position:fixed;bottom:40px;right:14px;background:var(--bg3);border:1px solid var(--line2);padding:8px 12px;border-radius:var(--r);font-size:12px;z-index:20;box-shadow:0 6px 20px rgba(0,0,0,.4)}
124 −.toast.err{border-color:var(--err)}
125 −@media (max-width:900px){.page{grid-template-columns:1fr;min-height:auto}.panel.left{border-right:0;border-bottom:1px solid var(--line)}.split{grid-template-columns:1fr}.nav a{padding:5px 7px}.grid2{grid-template-columns:1fr}.top{gap:10px;overflow-x:auto}.page.keys{grid-template-columns:1fr}}
174 +.chunk{border:1px solid var(--line);border-radius:var(--r);padding:10px;margin-bottom:10px;background:var(--bg2)}
175 +.chunk .h{font-family:var(--mono);font-size:11px;color:var(--fg3);margin-bottom:6px;display:flex;gap:10px;flex-wrap:wrap}
176 +.toast{position:fixed;bottom:calc(var(--foot) + var(--sab) + 12px);right:14px;left:auto;background:var(--fg);color:var(--bg2);border:0;padding:10px 14px;border-radius:var(--r);font-size:13px;z-index:40;box-shadow:var(--shadow-lg);max-width:min(92vw,420px)}
177 +.toast.err{background:var(--err);color:#fff}
178 +
179 +/* ---- mobile tab bar ---- */
180 +.tabbar{display:none}
181 +
182 +/* ---- responsive ---- */
183 +@media (max-width:1100px){.page{grid-template-columns:minmax(300px,360px) 1fr}.grid2{grid-template-columns:1fr 1fr}}
184 +@media (max-width:900px){
185 + :root{--top:50px}
186 + body{padding-bottom:calc(var(--tabbar) + var(--sab))}
187 + .nav{display:none}
188 + .top{gap:10px}
189 + .status span{display:none}
190 + .view{display:block;min-height:0;overflow-x:hidden}
191 + .page{display:flex;flex-direction:column;min-height:auto;min-width:0;width:100%}
192 + .panel{padding:14px;overflow:visible;width:100%}
193 + .panel.left{border-right:0;border-bottom:1px solid var(--line)}
194 + .panel.right{min-height:auto;display:block}
195 + .result{overflow:visible;min-height:0}
196 + .tabs{position:sticky;top:var(--top);z-index:5;background:var(--bg);padding-top:6px}
197 + .prose{max-width:100%}
198 + .prose table{display:block;overflow-x:auto}
199 + pre.code,.prose pre{max-width:100%}
200 + details.opts>summary{font-weight:600;color:var(--fg)}
201 + .tabs,#cr-detail,.panel.right{scroll-margin-top:calc(var(--top) + 4px)}
202 + .split{grid-template-columns:1fr;gap:10px}
203 + .split>div{max-height:45dvh}
204 + .grid2{grid-template-columns:1fr}
205 + .input,select.input,textarea.input{font-size:16px}
206 + .btn{min-height:44px}.icon-btn{width:38px;height:38px}
207 + .check{min-height:36px}
208 + .tabs{gap:0;margin:0 -14px;padding:0 14px 8px;flex-wrap:nowrap}
209 + .copy-group{padding-left:8px;border-left:1px solid var(--line);margin-left:6px}
210 + .result{padding:12px 0}
211 + .foot{display:none}
212 + .toast{left:14px;right:14px;max-width:none;bottom:calc(var(--tabbar) + var(--sab) + 12px)}
213 + .tbl{min-width:0}
214 + .tbl th:nth-child(5),.tbl td:nth-child(5){display:none}
215 + .vrow{grid-template-columns:22px 1fr 60px}
216 + .vrow .src:last-child{display:none}
217 + .ex-field,.ex-field.llm{grid-template-columns:1fr 1fr;grid-auto-rows:auto}
218 + .ex-field .link-btn{justify-self:end}
219 + .top-form .input{flex:1 1 100%}
220 + .top-form .btn{flex:1 1 100%}
221 + .tools .grow{flex:1 1 100%}
222 + .tabbar{position:fixed;left:0;right:0;bottom:0;z-index:30;display:grid;grid-template-columns:repeat(5,1fr);height:calc(var(--tabbar) + var(--sab));padding-bottom:var(--sab);background:color-mix(in srgb,var(--bg2) 92%,transparent);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-top:1px solid var(--line)}
223 + .tabbar a{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;color:var(--fg3);font-size:10.5px;font-weight:500;text-decoration:none;-webkit-tap-highlight-color:transparent}
224 + .tabbar a svg{width:22px;height:22px}
225 + .tabbar a.on{color:var(--accent2)}
226 + html[data-theme="dark"] .tabbar a.on{color:var(--accent)}
227 + .hist{margin-top:14px}
228 + .sticky-run{position:sticky;bottom:0;background:var(--bg2);padding-top:8px}
229 +}
230 +@media (max-width:480px){
231 + .brand span:not(.ver){display:inline}.brand .ver{display:none}
232 + .result-bar{gap:8px;font-size:11px}
233 + .prose{font-size:15px}.prose h1{font-size:21px}
234 + .panel{padding:12px}
235 + .page.keys .row.between{flex-direction:column;align-items:stretch;gap:8px}
236 + #ky-form{flex-wrap:wrap}#ky-form .input{flex:1 1 40%}
237 +}
238 +@media (prefers-reduced-motion:reduce){*{transition:none!important}}
126 239