Redesign the interface

Deep blue-black theme with a violet-to-cyan accent, frosted floating
panels, self-hosted Inter, a welcome screen with a call to action, a
card-style inspector, accent tabs and toggles, animated progress, and
a restyled open dialog. In 3D the WebGL walls are now lit per side
with a darker foot so buildings read as solids, and the canvas clears
to transparent over the new background. Renamed to CodeNavigator.
This commit is contained in:
Andy Dixon
2026-04-12 15:04:26 +01:00
parent 186f99f436
commit de3dbf8601
7 changed files with 370 additions and 271 deletions
+6
View File
@@ -12,6 +12,7 @@ import (
"io" "io"
"io/fs" "io/fs"
"log" "log"
"mime"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
@@ -71,6 +72,11 @@ type Server struct {
auth map[string]*githubAuth // by session ID auth map[string]*githubAuth // by session ID
} }
func init() {
// Alpine has no /etc/mime.types, so the embedded font needs an explicit type.
mime.AddExtensionType(".woff2", "font/woff2")
}
func NewServer(workspace string) *Server { func NewServer(workspace string) *Server {
cors := os.Getenv("CORS_ORIGIN") cors := os.Getenv("CORS_ORIGIN")
if cors == "" || strings.ContainsAny(cors, "\r\n") { if cors == "" || strings.ContainsAny(cors, "\r\n") {
+1 -1
View File
@@ -240,7 +240,7 @@ func TestRoutesAndHeaders(t *testing.T) {
t.Fatalf("preflight = %d %v", res.StatusCode, res.Header) t.Fatalf("preflight = %d %v", res.StatusCode, res.Header)
} }
for path, contentType := range map[string]string{"/": "text/html", "/api.mjs": "text/javascript", "/styles.css": "text/css", "/config.js": "text/javascript"} { for path, contentType := range map[string]string{"/": "text/html", "/api.mjs": "text/javascript", "/styles.css": "text/css", "/config.js": "text/javascript", "/inter-latin.woff2": "font/woff2"} {
res, err := http.Get(c.url + path) res, err := http.Get(c.url + path)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
+12 -10
View File
@@ -170,7 +170,7 @@ function applySnapshot(snapshot){
fileById=new Map(files.map(file=>[file.id,file]));referenceDegree=new Map();edgesByFile=new Map();let known=0,inferred=0;for(const edge of edges){referenceDegree.set(edge.from,(referenceDegree.get(edge.from)||0)+1);referenceDegree.set(edge.to,(referenceDegree.get(edge.to)||0)+1);for(const id of [edge.from,edge.to]){const list=edgesByFile.get(id);if(list)list.push(edge);else edgesByFile.set(id,[edge]);}if(edge.confidence==='known')known++;else inferred++;} fileById=new Map(files.map(file=>[file.id,file]));referenceDegree=new Map();edgesByFile=new Map();let known=0,inferred=0;for(const edge of edges){referenceDegree.set(edge.from,(referenceDegree.get(edge.from)||0)+1);referenceDegree.set(edge.to,(referenceDegree.get(edge.to)||0)+1);for(const id of [edge.from,edge.to]){const list=edgesByFile.get(id);if(list)list.push(edge);else edgesByFile.set(id,[edge]);}if(edge.confidence==='known')known++;else inferred++;}
const layers=new Map();for(const file of files){const layer=file.layer in layerLabels?file.layer:'unknown';layers.set(layer,(layers.get(layer)||0)+1);}sceneStats={definitions:snapshot.definitions??files.reduce((n,file)=>n+(file.symbolCount||0),0),totalLines:snapshot.totalLines??files.reduce((n,file)=>n+file.lines,0),known,inferred,layers}; const layers=new Map();for(const file of files){const layer=file.layer in layerLabels?file.layer:'unknown';layers.set(layer,(layers.get(layer)||0)+1);}sceneStats={definitions:snapshot.definitions??files.reduce((n,file)=>n+(file.symbolCount||0),0),totalLines:snapshot.totalLines??files.reduce((n,file)=>n+file.lines,0),known,inferred,layers};
computeLayout();fitScene();updateStats(snapshot);renderInspector();renderResults([]);renderHistory(); computeLayout();fitScene();updateStats(snapshot);renderInspector();renderResults([]);renderHistory();
$('#breadcrumbText').textContent=currentName;$('#emptyState').hidden=files.length>0; $('#breadcrumbText').textContent=currentName;$('#emptyState').hidden=files.length>0;$('#welcome').hidden=true;
} }
function fitScene(){ function fitScene(){
@@ -203,7 +203,9 @@ function updateStats(snapshot={}){
function format(value){return Number(value||0).toLocaleString();} function format(value){return Number(value||0).toLocaleString();}
function escapeHtml(value=''){return String(value).replace(/[&<>"]/g,char=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[char]));} function escapeHtml(value=''){return String(value).replace(/[&<>"]/g,char=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[char]));}
const syntaxPalette={plain:'#aab5af',comment:'#64726b',string:'#89b88f',number:'#83a8d8',keyword:'#d8a75b',type:'#79b8d8',function:'#d6cf8b',property:'#b397d5',operator:'#8d9c95',constant:'#d98585',tag:'#65b9aa'}; const syntaxPalette={plain:'#b6bfd0',comment:'#5c667a',string:'#8fd694',number:'#f5a97f',keyword:'#c69cff',type:'#6ec6ff',function:'#7aa7ff',property:'#5fd3c6',operator:'#8f9ab0',constant:'#ff8fa3',tag:'#5fd3c6'};
// Matches the renderer's tile top so code drawn over a 3D roof blends with the WebGL face.
const TILE_TOP='#0e1118';
const syntaxKeywords=new Set(`abstract as async await break case catch class const continue crate def default defer delete do else enum export extends extern false final finally fn for from func function get go if impl import in instanceof interface is lambda let loop match mod module mut namespace new nil None null of override package private protected pub public raise readonly require return self set static struct super switch this throw trait true try type typeof undefined union unsafe use using var virtual void where while with yield`.split(' ')); const syntaxKeywords=new Set(`abstract as async await break case catch class const continue crate def default defer delete do else enum export extends extern false final finally fn for from func function get go if impl import in instanceof interface is lambda let loop match mod module mut namespace new nil None null of override package private protected pub public raise readonly require return self set static struct super switch this throw trait true try type typeof undefined union unsafe use using var virtual void where while with yield`.split(' '));
const syntaxConstants=new Set('true false null nil none undefined nan infinity self this super'.split(' ')); const syntaxConstants=new Set('true false null nil none undefined nan infinity self this super'.split(' '));
const tokenPattern=/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\b0x[\da-f]+\b|\b0b[01]+\b|#[\da-f]{3,8}\b|\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b|[A-Za-z_$][\w$-]*|\s+|./gi; const tokenPattern=/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\b0x[\da-f]+\b|\b0b[01]+\b|#[\da-f]{3,8}\b|\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b|[A-Za-z_$][\w$-]*|\s+|./gi;
@@ -317,7 +319,7 @@ function selectFile(file,addHistory=true){
function renderInspector(){ function renderInspector(){
const hint=$('#inspectHint'),content=$('#inspectContent'); const hint=$('#inspectHint'),content=$('#inspectContent');
if(!files.length){hint.hidden=false;hint.textContent='Open a local folder or public GitHub repository to begin.';content.innerHTML='';return;} if(!files.length){hint.hidden=false;hint.textContent='Open a local folder or GitHub repository to begin.';content.innerHTML='';return;}
if(!selected){ if(!selected){
hint.hidden=false;hint.textContent=expandedCoverageLayer?'Click a file or use ↑ and ↓ to move around the map':'Select an entity on the map'; hint.hidden=false;hint.textContent=expandedCoverageLayer?'Click a file or use ↑ and ↓ to move around the map':'Select an entity on the map';
const coverageRows=Object.entries(layerLabels).map(([key,label])=>`<button class="coverage-row${expandedCoverageLayer===key?' active':''}" type="button" data-coverage-layer="${key}" aria-expanded="${expandedCoverageLayer===key}"><i style="background:${palettes[paletteIndex][key]}"></i><span>${label}</span><small>${format(sceneStats.layers.get(key)||0)}</small><b aria-hidden="true">${expandedCoverageLayer===key?'−':'+'}</b></button>`).join(''); const coverageRows=Object.entries(layerLabels).map(([key,label])=>`<button class="coverage-row${expandedCoverageLayer===key?' active':''}" type="button" data-coverage-layer="${key}" aria-expanded="${expandedCoverageLayer===key}"><i style="background:${palettes[paletteIndex][key]}"></i><span>${label}</span><small>${format(sceneStats.layers.get(key)||0)}</small><b aria-hidden="true">${expandedCoverageLayer===key?'−':'+'}</b></button>`).join('');
@@ -406,7 +408,7 @@ $$('.tab').forEach(tab=>tab.addEventListener('click',()=>switchTab(tab.dataset.t
function resizeOverlay(){const dpr=Math.min(devicePixelRatio||1,2),w=Math.floor(overlay.clientWidth*dpr),h=Math.floor(overlay.clientHeight*dpr);if(overlay.width!==w||overlay.height!==h){overlay.width=w;overlay.height=h;}overlayCtx.setTransform(dpr,0,0,dpr,0,0);} function resizeOverlay(){const dpr=Math.min(devicePixelRatio||1,2),w=Math.floor(overlay.clientWidth*dpr),h=Math.floor(overlay.clientHeight*dpr);if(overlay.width!==w||overlay.height!==h){overlay.width=w;overlay.height=h;}overlayCtx.setTransform(dpr,0,0,dpr,0,0);}
function drawChip(ctx,text,x,y,maxWidth,accent,occupied,force=false,align='left'){ function drawChip(ctx,text,x,y,maxWidth,accent,occupied,force=false,align='left'){
if(maxWidth<24)return false; if(maxWidth<24)return false;
ctx.font='600 11px -apple-system, BlinkMacSystemFont, sans-serif'; ctx.font='600 11px Inter, system-ui, sans-serif';
let label=text; let label=text;
if(ctx.measureText(label).width>maxWidth-10){let low=1,high=label.length;while(low<high){const mid=Math.ceil((low+high)/2);if(ctx.measureText(`${label.slice(0,mid)}…`).width<=maxWidth-10)low=mid;else high=mid-1;}label=`${label.slice(0,low)}…`;} if(ctx.measureText(label).width>maxWidth-10){let low=1,high=label.length;while(low<high){const mid=Math.ceil((low+high)/2);if(ctx.measureText(`${label.slice(0,mid)}…`).width<=maxWidth-10)low=mid;else high=mid-1;}label=`${label.slice(0,low)}…`;}
const width=Math.min(maxWidth,ctx.measureText(label).width+10); const width=Math.min(maxWidth,ctx.measureText(label).width+10);
@@ -414,7 +416,7 @@ function drawChip(ctx,text,x,y,maxWidth,accent,occupied,force=false,align='left'
const box={x:x-3,y:y-2,w:width+6,h:21}; const box={x:x-3,y:y-2,w:width+6,h:21};
if(!force&&occupied?.some(other=>box.x<other.x+other.w&&box.x+box.w>other.x&&box.y<other.y+other.h&&box.y+box.h>other.y))return false; if(!force&&occupied?.some(other=>box.x<other.x+other.w&&box.x+box.w>other.x&&box.y<other.y+other.h&&box.y+box.h>other.y))return false;
occupied?.push(box); occupied?.push(box);
ctx.fillStyle='rgba(27,30,28,.9)';ctx.fillRect(x,y,width,17); ctx.fillStyle='rgba(10,12,18,.88)';ctx.fillRect(x,y,width,17);
if(accent){ctx.fillStyle=accent;ctx.fillRect(x,y,2,17);} if(accent){ctx.fillStyle=accent;ctx.fillRect(x,y,2,17);}
ctx.fillStyle='rgba(237,241,238,.94)';ctx.fillText(label,x+5,y+2); ctx.fillStyle='rgba(237,241,238,.94)';ctx.fillText(label,x+5,y+2);
return true; return true;
@@ -426,7 +428,7 @@ function drawFileChip(ctx,item,rect,accent,occupied,force=false){
return drawChip(ctx,item.name,anchor.x,anchor.y-8,maxWidth,accent,occupied,force,'center'); return drawChip(ctx,item.name,anchor.x,anchor.y-8,maxWidth,accent,occupied,force,'center');
} }
function drawOverlay(){ function drawOverlay(){
resizeOverlay();const ctx=overlayCtx,w=overlay.clientWidth,h=overlay.clientHeight;ctx.clearRect(0,0,w,h);ctx.save();ctx.font='600 11px -apple-system, BlinkMacSystemFont, sans-serif';ctx.textBaseline='top'; resizeOverlay();const ctx=overlayCtx,w=overlay.clientWidth,h=overlay.clientHeight;ctx.clearRect(0,0,w,h);ctx.save();ctx.font='600 11px Inter, system-ui, sans-serif';ctx.textBaseline='top';
codeTexturesPending=false; codeTexturesPending=false;
codeTextureDeadline=performance.now()+5; codeTextureDeadline=performance.now()+5;
const hitIds=new Set(searchHits.map(hit=>hit.entityId??hit.id)),occupied=[],visible=visibleLayout(w,h);visibleScreenItems=visible; const hitIds=new Set(searchHits.map(hit=>hit.entityId??hit.id)),occupied=[],visible=visibleLayout(w,h);visibleScreenItems=visible;
@@ -454,7 +456,7 @@ function drawOverlay(){
if(rect.w>46&&rect.h>18&&codeScreenFont(item)<5.2&&labelCount<110&&drawFileChip(ctx,item,rect,null,occupied))labelCount++; if(rect.w>46&&rect.h>18&&codeScreenFont(item)<5.2&&labelCount<110&&drawFileChip(ctx,item,rect,null,occupied))labelCount++;
} }
if(hitIds.size){ if(hitIds.size){
ctx.fillStyle='rgba(11,13,12,.48)';ctx.fillRect(0,0,w,h); ctx.fillStyle='rgba(5,6,10,.55)';ctx.fillRect(0,0,w,h);
for(const {item,rect} of visible){if(!hitIds.has(item.id))continue;ctx.strokeStyle='#e8c74b';ctx.lineWidth=2;ctx.strokeRect(rect.x-.5,rect.y-.5,rect.w+1,rect.h+1);drawFileChip(ctx,item,rect,'#e8c74b',null,true);} for(const {item,rect} of visible){if(!hitIds.has(item.id))continue;ctx.strokeStyle='#e8c74b';ctx.lineWidth=2;ctx.strokeRect(rect.x-.5,rect.y-.5,rect.w+1,rect.h+1);drawFileChip(ctx,item,rect,'#e8c74b',null,true);}
} }
drawSelectedConnections(ctx); drawSelectedConnections(ctx);
@@ -465,7 +467,7 @@ function drawOverlay(){
function tileCorners(item){return [[item.x,item.y],[item.x+item.w,item.y],[item.x+item.w,item.y+item.h],[item.x,item.y+item.h]].map(([x,y])=>renderer.project([x,y,item.height||0]));} function tileCorners(item){return [[item.x,item.y],[item.x+item.w,item.y],[item.x+item.w,item.y+item.h],[item.x,item.y+item.h]].map(([x,y])=>renderer.project([x,y,item.height||0]));}
function drawGroundGrid(ctx){ function drawGroundGrid(ctx){
if(!layoutItems.length)return; if(!layoutItems.length)return;
ctx.save();ctx.globalCompositeOperation='destination-over';ctx.strokeStyle='rgba(180,180,180,.12)';ctx.lineWidth=.75;ctx.beginPath(); ctx.save();ctx.globalCompositeOperation='destination-over';ctx.strokeStyle='rgba(150,160,255,.11)';ctx.lineWidth=.75;ctx.beginPath();
for(let x=-200;x<=1200;x+=100){const a=renderer.project([x,-200,0]),b=renderer.project([x,900,0]);ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);} for(let x=-200;x<=1200;x+=100){const a=renderer.project([x,-200,0]),b=renderer.project([x,900,0]);ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);}
for(let y=-200;y<=900;y+=100){const a=renderer.project([-200,y,0]),b=renderer.project([1200,y,0]);ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);} for(let y=-200;y<=900;y+=100){const a=renderer.project([-200,y,0]),b=renderer.project([1200,y,0]);ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);}
ctx.stroke(); ctx.stroke();
@@ -556,7 +558,7 @@ function draw3dCode(ctx,visible){
for(const {item,rect} of faces){ for(const {item,rect} of faces){
const corners=tileCorners(item);if(corners.some(p=>!Number.isFinite(p.x)||!Number.isFinite(p.y)))continue; const corners=tileCorners(item);if(corners.some(p=>!Number.isFinite(p.x)||!Number.isFinite(p.y)))continue;
ctx.save();ctx.beginPath();corners.forEach((p,i)=>i?ctx.lineTo(p.x,p.y):ctx.moveTo(p.x,p.y));ctx.closePath();ctx.clip(); ctx.save();ctx.beginPath();corners.forEach((p,i)=>i?ctx.lineTo(p.x,p.y):ctx.moveTo(p.x,p.y));ctx.closePath();ctx.clip();
ctx.fillStyle='#1c1c1c';ctx.fillRect(rect.x-1,rect.y-1,rect.w+2,rect.h+2); ctx.fillStyle=TILE_TOP;ctx.fillRect(rect.x-1,rect.y-1,rect.w+2,rect.h+2);
const texture=buildCodeTexture(item,rect); const texture=buildCodeTexture(item,rect);
if(texture){ if(texture){
// Match the two triangles used by the WebGL top face exactly. // Match the two triangles used by the WebGL top face exactly.
@@ -692,7 +694,7 @@ document.addEventListener('keydown',event=>{
}); });
window.addEventListener('resize',()=>{dirty=true;}); window.addEventListener('resize',()=>{dirty=true;});
const dialog=$('#loadDialog');$('#loadButton').addEventListener('click',()=>dialog.showModal()); const dialog=$('#loadDialog');$('#loadButton').addEventListener('click',()=>dialog.showModal());$('#welcomeOpen').addEventListener('click',()=>dialog.showModal());
$('#localFolderButton').addEventListener('click',async()=>{ $('#localFolderButton').addEventListener('click',async()=>{
dialog.close(); dialog.close();
if('showDirectoryPicker' in window){try{const handle=await window.showDirectoryPicker();const chosen=[];await collectHandles(handle,'',chosen);await uploadLocal(handle.name,chosen);}catch(error){if(error.name!=='AbortError')showError(error.message);}} if('showDirectoryPicker' in window){try{const handle=await window.showDirectoryPicker();const chosen=[];await collectHandles(handle,'',chosen);await uploadLocal(handle.name,chosen);}catch(error){if(error.name!=='AbortError')showError(error.message);}}
+10 -2
View File
@@ -3,16 +3,17 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"> <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="theme-color" content="#1b1b1b"> <meta name="theme-color" content="#0b0d13">
<meta name="description" content="Explore an entire codebase as a fast, searchable 2D and 3D landscape."> <meta name="description" content="Explore an entire codebase as a fast, searchable 2D and 3D landscape.">
<title>CodeNavigator</title> <title>CodeNavigator</title>
<link rel="icon" href="./favicon.svg" type="image/svg+xml"> <link rel="icon" href="./favicon.svg" type="image/svg+xml">
<link rel="preload" href="./inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="./styles.css"> <link rel="stylesheet" href="./styles.css">
</head> </head>
<body> <body>
<main id="app"> <main id="app">
<header class="toolbar" aria-label="Codebase controls"> <header class="toolbar" aria-label="Codebase controls">
<div class="brand" aria-label="CodeNavigator"><svg viewBox="0 0 28 28" aria-hidden="true"><rect x="3" y="3" width="9" height="22" rx="2"/><rect x="16" y="3" width="9" height="9" rx="2"/><rect x="16" y="16" width="9" height="9" rx="2"/></svg><span>Codebase<span class="brand-sub">VIEWER</span></span></div> <div class="brand" aria-label="CodeNavigator"><svg viewBox="0 0 28 28" aria-hidden="true"><defs><linearGradient id="brandGradient" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#9a7dff"/><stop offset=".5" stop-color="#5f9dff"/><stop offset="1" stop-color="#36d3c2"/></linearGradient></defs><rect x="3" y="11" width="6" height="14" rx="1.6" fill="url(#brandGradient)" opacity=".7"/><rect x="11" y="3" width="6" height="22" rx="1.6" fill="url(#brandGradient)"/><rect x="19" y="15" width="6" height="10" rx="1.6" fill="url(#brandGradient)" opacity=".5"/></svg><span class="brand-name">Code<em>Navigator</em></span></div>
<div class="tool-group"> <div class="tool-group">
<button class="tool primary" id="loadButton" title="Open codebase" aria-label="Open codebase"><svg viewBox="0 0 24 24"><path d="M3.5 7.5h6l2-2h4l2 2h3v11h-17z"/><path d="M3.5 9.5h17"/></svg></button> <button class="tool primary" id="loadButton" title="Open codebase" aria-label="Open codebase"><svg viewBox="0 0 24 24"><path d="M3.5 7.5h6l2-2h4l2 2h3v11h-17z"/><path d="M3.5 9.5h17"/></svg></button>
<button class="tool active" id="mapButton" title="Code landscape" aria-label="Code landscape"><svg viewBox="0 0 24 24"><rect x="3.5" y="3.5" width="17" height="17" rx="2"/><path d="M3.5 10h17M10 3.5v17M10 15h10.5"/></svg></button> <button class="tool active" id="mapButton" title="Code landscape" aria-label="Code landscape"><svg viewBox="0 0 24 24"><rect x="3.5" y="3.5" width="17" height="17" rx="2"/><path d="M3.5 10h17M10 3.5v17M10 15h10.5"/></svg></button>
@@ -48,6 +49,13 @@
<div class="viewport" id="viewport"> <div class="viewport" id="viewport">
<canvas id="glCanvas"></canvas> <canvas id="glCanvas"></canvas>
<canvas id="overlayCanvas"></canvas> <canvas id="overlayCanvas"></canvas>
<div class="welcome" id="welcome">
<span class="eyebrow"><i></i>Codebase landscapes</span>
<h1>See your code<br><span>as a place.</span></h1>
<p>Every file becomes a tile sized by its lines, grouped by folder and coloured by purpose. Fly over it in 2D or 3D, search every symbol, and follow imports from file to file.</p>
<div class="welcome-actions"><button type="button" class="button-primary" id="welcomeOpen">Open a codebase</button><span><kbd>Ctrl</kbd> <kbd>O</kbd></span></div>
<div class="welcome-features"><span>Public &amp; private GitHub repos</span><span>Local folders</span><span>Symbol search</span><span>Import graph</span></div>
</div>
<div class="empty-state" id="emptyState" hidden> <div class="empty-state" id="emptyState" hidden>
<strong>No source files found</strong> <strong>No source files found</strong>
<span>Try another folder or public GitHub repository.</span> <span>Try another folder or public GitHub repository.</span>
Binary file not shown.
+11 -3
View File
@@ -62,8 +62,16 @@ void main(){
vec2 face = vFace < .5 || vFace > 4.5 ? vUnit.xy : vFace < 2.5 ? vUnit.xz : vUnit.yz; vec2 face = vFace < .5 || vFace > 4.5 ? vUnit.xy : vFace < 2.5 ? vUnit.xz : vUnit.yz;
float edge = min(min(face.x, 1.0-face.x), min(face.y, 1.0-face.y)); float edge = min(min(face.x, 1.0-face.x), min(face.y, 1.0-face.y));
float border = smoothstep(0.0, fwidth(edge)*1.35 + .009, edge); float border = smoothstep(0.0, fwidth(edge)*1.35 + .009, edge);
vec3 base = vec3(.11); // Tile tops match TILE_TOP in app.js so source drawn over a roof blends in; 2D gets a faint layer tint.
vec3 stroke = mix(vColor.rgb, vec3(1.0), .07); vec3 top = vec3(.055, .067, .094);
vec3 base = uMode < .5 ? mix(top, vColor.rgb, .07) : top;
vec3 stroke = mix(vColor.rgb, vec3(1.0), .12);
if(vFace > .5 && vFace < 4.5){
// Walls: a fixed key light per side and a darker foot make buildings read as solids.
float light = vFace < 1.5 ? .34 : vFace < 2.5 ? .2 : vFace < 3.5 ? .26 : .42;
base = mix(top, vColor.rgb, light) * (.55 + .45 * vUnit.z);
stroke = mix(stroke, base, .45);
}
outColor = vec4(mix(stroke, base, border), uAlpha); outColor = vec4(mix(stroke, base, border), uAlpha);
}`; }`;
@@ -114,7 +122,7 @@ export class LandscapeRenderer {
render(alpha=1) { render(alpha=1) {
const gl=this.gl; this.resize(); const gl=this.gl; this.resize();
gl.clearColor(.11,.11,.11,1); gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT); gl.clearColor(0,0,0,0); gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA); gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA);
gl.useProgram(this.program); gl.bindVertexArray(this.vao); gl.useProgram(this.program); gl.bindVertexArray(this.vao);
const u=n=>gl.getUniformLocation(this.program,n); const u=n=>gl.getUniformLocation(this.program,n);
+330 -255
View File
@@ -1,24 +1,37 @@
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400 700;
font-display: swap;
src: url("./inter-latin.woff2") format("woff2");
}
:root { :root {
color-scheme: dark; color-scheme: dark;
--bg: #212121; --bg: #07090d;
--bg-deep: #161616; --bg-raised: #0d1017;
--panel: #1e1e1e; --surface: rgba(16, 19, 27, .74);
--panel-2: #262626; --surface-solid: #10131b;
--line: #3d3d3d; --surface-2: rgba(255, 255, 255, .035);
--line-soft: #2f2f2f; --hover: rgba(255, 255, 255, .06);
--text: #e2e2e2; --line: rgba(255, 255, 255, .08);
--muted: #a3a3a3; --line-strong: rgba(255, 255, 255, .15);
--dim: #626262; --text: #e9ecf3;
--cyan: #65b3ff; --muted: #9ba3b4;
--green: #29c997; --dim: #687083;
--yellow: #d6b23d; --accent: #8d7dff;
--orange: #d67d42; --accent-2: #36d3c2;
--pink: #cf6fa4; --accent-grad: linear-gradient(135deg, #9a7dff 0%, #5f9dff 50%, #36d3c2 100%);
--blue: #488ad2; --accent-soft: rgba(141, 125, 255, .16);
--danger: #ef6b69; --accent-line: rgba(141, 125, 255, .5);
--panel-width: 280px; --green: #36d3a0;
--accent: #cdcdcd; --yellow: #f2c94c;
--toolbar-height: 46px; --danger: #ff6b7a;
--glass: blur(18px) saturate(150%);
--shadow: 0 18px 50px rgba(0, 0, 0, .45), 0 2px 8px rgba(0, 0, 0, .3);
--panel-width: 300px;
--toolbar-height: 52px;
--mono: "JetBrains Mono", "SFMono-Regular", ui-monospace, Consolas, monospace;
} }
* { box-sizing: border-box; } * { box-sizing: border-box; }
@@ -26,296 +39,358 @@
html, body, #app { width: 100%; height: 100%; overflow: hidden; } html, body, #app { width: 100%; height: 100%; overflow: hidden; }
body { body {
margin: 0; margin: 0;
background: var(--bg-deep); background: var(--bg);
color: var(--text); color: var(--text);
font: 12px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, sans-serif; font: 12.5px/1.5 Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
font-feature-settings: "cv11", "ss01";
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
letter-spacing: .005em;
} }
button, input { font: inherit; } button, input, select { font: inherit; }
button { color: inherit; } button { color: inherit; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; } .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
::selection { background: rgba(141, 125, 255, .35); color: #fff; }
* { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.14) transparent; }
kbd { font: 500 10px/1.5 var(--mono); border: 1px solid var(--line-strong); border-bottom-width: 2px; border-radius: 5px; padding: 0 5px; color: #cfd5e2; background: rgba(255,255,255,.04); }
/* Toolbar */
.toolbar { .toolbar {
position: relative; position: relative;
z-index: 5; z-index: 5;
height: var(--toolbar-height); height: var(--toolbar-height);
padding: 0 10px; padding: 0 14px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 7px; gap: 8px;
background: linear-gradient(180deg, #222222, #1d1d1d); background: linear-gradient(180deg, rgba(18, 21, 30, .96), rgba(11, 13, 19, .96));
border-bottom: 1px solid #383838; border-bottom: 1px solid var(--line);
box-shadow: 0 4px 20px #0002;
} }
.toolbar::after {
content: "";
position: absolute;
left: 0; right: 0; bottom: -1px;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(141,125,255,.55) 30%, rgba(54,211,194,.45) 70%, transparent);
opacity: .55;
}
.brand { display: flex; gap: 10px; align-items: center; padding-right: 14px; margin-right: 2px; border-right: 1px solid var(--line); }
.brand svg { width: 26px; height: 26px; filter: drop-shadow(0 0 10px rgba(141,125,255,.45)); }
.brand-name { font-size: 15px; font-weight: 650; letter-spacing: -.03em; line-height: 1; color: #f2f4f9; }
.brand-name em { font-style: normal; background: var(--accent-grad); -webkit-background-clip: text; background-clip: text; color: transparent; }
.tool-group { display: flex; align-items: center; gap: 3px; } .tool-group { display: flex; align-items: center; gap: 3px; }
.tool, .metric { .tool, .metric {
height: 28px; height: 30px;
min-width: 28px; min-width: 30px;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 8px; border-radius: 9px;
background: transparent; background: transparent;
color: #a9a9a9; color: var(--muted);
cursor: pointer; cursor: pointer;
transition: 100ms ease; transition: background-color .15s, border-color .15s, color .15s, box-shadow .15s;
} }
.tool { width: 28px; padding: 0; display: grid; place-items: center; } .tool { width: 30px; padding: 0; display: grid; place-items: center; }
.tool svg { width: 15px; height: 15px; display: block; margin: 0; fill: none; stroke: currentColor; stroke-width: 1.65; stroke-linecap: round; stroke-linejoin: round; } .tool svg { width: 16px; height: 16px; display: block; fill: none; stroke: currentColor; stroke-width: 1.65; stroke-linecap: round; stroke-linejoin: round; }
.tool-link { display: grid; place-items: center; text-decoration: none; } .tool-link { text-decoration: none; }
.github-link svg { width: 16px; height: 16px; margin: 0; fill: currentColor; stroke: none; } .github-link svg { fill: currentColor; stroke: none; }
.metric b { font: 500 15px/1 "SFMono-Regular", Consolas, monospace; } .metric { padding: 0 10px; display: flex; gap: 6px; align-items: center; font-weight: 500; }
.tool:hover, .metric:hover { color: #e8e8e8; background: #313131; } .metric b { font: 500 13px/1 var(--mono); opacity: .8; }
.tool.active, .metric.active { color: #dadada; border-color: #525252; background: #333333; box-shadow: inset 0 1px #ffffff09; } .tool:hover, .metric:hover { color: var(--text); background: var(--hover); }
.tool.primary { border-color: #c7c7c7; color: #262626; background: #cdcdcd; } .tool.active, .metric.active {
.tool.primary:hover { background: #dedede; color: #1a1a1a; } color: #fff;
border-color: var(--accent-line);
background: linear-gradient(180deg, rgba(141,125,255,.24), rgba(141,125,255,.12));
box-shadow: 0 0 0 1px rgba(141,125,255,.08), 0 4px 14px rgba(141,125,255,.22), inset 0 1px rgba(255,255,255,.12);
}
.tool.primary {
color: #fff;
border-color: transparent;
background: var(--accent-grad);
box-shadow: 0 4px 16px rgba(95,157,255,.35), inset 0 1px rgba(255,255,255,.25);
}
.tool.primary:hover { filter: brightness(1.1); background: var(--accent-grad); }
.tool.palette { border-radius: 50%; } .tool.palette { border-radius: 50%; }
.separator { width: 1px; height: 22px; background: #3c3c3c; margin: 0 4px; } .view-switch, .metric-switch { display: flex; align-items: center; padding: 3px; border: 1px solid var(--line); border-radius: 12px; background: rgba(0,0,0,.28); gap: 2px; }
.metric-switch { display: flex; align-items: center; } .separator { width: 1px; height: 24px; background: var(--line); margin: 0 3px; }
.metric { padding: 0 8px; display: flex; gap: 5px; align-items: center; }
.toolbar-spacer { flex: 1; } .toolbar-spacer { flex: 1; }
.scene-stats { text-align: right; color: #b0b0b0; line-height: 1.25; min-width: 210px; } .scene-stats { text-align: right; color: var(--text); line-height: 1.25; margin-right: 8px; font-size: 11.5px; font-weight: 500; font-variant-numeric: tabular-nums; }
.scene-stats small { display: block; color: #6b6b6b; } .scene-stats small { display: block; margin-top: 3px; color: var(--dim); font-weight: 400; }
.filter-wrap { position: relative; width: min(230px, 20vw); } .filter-wrap { position: relative; width: min(260px, 22vw); }
.filter-wrap input { .filter-wrap::before {
width: 100%; height: 29px; padding: 0 30px 0 10px; content: "";
border: 1px solid #424242; border-radius: 7px; position: absolute; left: 11px; top: 50%;
outline: none; color: #e3e3e3; background: #313131; width: 12px; height: 12px; transform: translateY(-50%);
background: currentColor; color: var(--dim);
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m20 20-3.5-3.5'/%3E%3C/svg%3E") center / contain no-repeat;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m20 20-3.5-3.5'/%3E%3C/svg%3E") center / contain no-repeat;
pointer-events: none;
} }
.filter-wrap input:focus { border-color: #999999; box-shadow: 0 0 0 2px rgba(71,145,210,.13); } .filter-wrap input {
.filter-wrap input::placeholder { color: #848484; } width: 100%; height: 34px; padding: 0 34px 0 31px;
.filter-wrap kbd { position: absolute; right: 8px; top: 6px; color: #727272; font: 10px monospace; } border: 1px solid var(--line); border-radius: 10px;
.live-dot { width: 7px; height: 7px; border-radius: 50%; background: #868686; box-shadow: 0 0 0 2px rgba(237,69,96,.12); } outline: none; color: var(--text); background: rgba(0,0,0,.3);
.live-dot.connected { background: var(--green); box-shadow: 0 0 0 2px rgba(41,201,151,.12); } transition: border-color .15s, box-shadow .15s, background-color .15s;
}
.filter-wrap input::placeholder { color: var(--dim); }
.filter-wrap input:focus { border-color: var(--accent-line); background: rgba(0,0,0,.45); box-shadow: 0 0 0 4px rgba(141,125,255,.14); }
.filter-wrap kbd { position: absolute; right: 9px; top: 50%; transform: translateY(-50%); color: var(--dim); }
.live-dot { flex-shrink: 0; width: 8px; height: 8px; margin-left: 4px; border-radius: 50%; background: #5d6475; }
.live-dot.connected { background: var(--green); box-shadow: 0 0 0 3px rgba(54,211,160,.14), 0 0 10px rgba(54,211,160,.6); animation: pulse 2.6s ease-in-out infinite; }
@keyframes pulse { 50% { box-shadow: 0 0 0 5px rgba(54,211,160,.05), 0 0 14px rgba(54,211,160,.35); } }
/* Workspace and viewport */
.workspace { position: absolute; inset: var(--toolbar-height) 0 0; display: grid; grid-template-columns: minmax(0,1fr) var(--panel-width); } .workspace { position: absolute; inset: var(--toolbar-height) 0 0; display: grid; grid-template-columns: minmax(0,1fr) var(--panel-width); }
.viewport { position: relative; min-width: 0; overflow: hidden; touch-action: none; user-select: none; background-color: #212121; background-image: linear-gradient(45deg,rgba(255,255,255,.012) 25%,transparent 25%),linear-gradient(-45deg,rgba(255,255,255,.012) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(255,255,255,.012) 75%),linear-gradient(-45deg,transparent 75%,rgba(255,255,255,.012) 75%); background-size: 24px 24px; background-position: 0 0,0 12px,12px -12px,-12px 0; cursor: grab; } .viewport {
position: relative; min-width: 0; overflow: hidden; touch-action: none; user-select: none; cursor: grab;
background:
radial-gradient(1200px 700px at 30% -10%, rgba(141,125,255,.13), transparent 60%),
radial-gradient(900px 600px at 90% 110%, rgba(54,211,194,.09), transparent 60%),
radial-gradient(circle at 1px 1px, rgba(255,255,255,.045) 1px, transparent 0) 0 0 / 22px 22px,
var(--bg);
}
.viewport.dragging { cursor: grabbing; } .viewport.dragging { cursor: grabbing; }
.viewport.panning { cursor: move; } .viewport.panning { cursor: move; }
#glCanvas, #overlayCanvas { position: absolute; inset: 0; width: 100%; height: 100%; } #glCanvas, #overlayCanvas { position: absolute; inset: 0; width: 100%; height: 100%; }
#overlayCanvas { pointer-events: none; } #overlayCanvas { pointer-events: none; }
.inspector { background: linear-gradient(160deg,#222222,#1d1d1d); border-left: 1px solid #393939; min-width: 0; overflow: hidden; }
.tabs { height: 40px; display: flex; align-items: center; gap: 4px; padding: 0 10px; border-bottom: 1px solid var(--line-soft); } .breadcrumb, .canvas-controls, .camera-help, .legend, .tooltip, .progress-card {
.tab { height: 27px; padding: 0 9px; border: 0; border-radius: 7px; background: transparent; color: #9c9c9c; cursor: pointer; } border: 1px solid var(--line);
.tab:hover { color: #bbbbbb; } background: var(--surface);
.tab.active { color: #dedede; background: #333333; box-shadow: inset 0 0 0 1px #454545; } -webkit-backdrop-filter: var(--glass);
#resultBadge:not(:empty) { margin-left: 3px; padding: 1px 5px; border-radius: 8px; background: #404040; font-size: 9px; } backdrop-filter: var(--glass);
.panel { display: none; height: calc(100% - 40px); overflow: auto; padding: 13px 12px 20px; scrollbar-color: #545454 transparent; scrollbar-width: thin; } box-shadow: var(--shadow), inset 0 1px rgba(255,255,255,.05);
}
/* Welcome */
.welcome { position: absolute; inset: 0; z-index: 1; display: grid; place-content: center; justify-items: start; padding: 32px; max-width: 760px; margin: 0 auto; cursor: default; }
.welcome .eyebrow { display: inline-flex; align-items: center; gap: 8px; padding: 5px 11px 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: rgba(255,255,255,.03); color: var(--muted); font-size: 11px; font-weight: 500; }
.welcome .eyebrow i { width: 7px; height: 7px; border-radius: 50%; background: var(--accent-grad); box-shadow: 0 0 10px rgba(141,125,255,.8); }
.welcome h1 { margin: 20px 0 0; font-size: clamp(34px, 5.2vw, 60px); line-height: 1.02; font-weight: 700; letter-spacing: -.045em; color: #f4f6fb; }
.welcome h1 span { background: var(--accent-grad); -webkit-background-clip: text; background-clip: text; color: transparent; }
.welcome p { max-width: 520px; margin: 18px 0 0; color: var(--muted); font-size: 15px; line-height: 1.6; }
.welcome-actions { display: flex; align-items: center; gap: 16px; margin-top: 30px; color: var(--dim); font-size: 11px; }
.welcome-features { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 36px; }
.welcome-features span { padding: 6px 11px; border: 1px solid var(--line); border-radius: 999px; background: rgba(255,255,255,.02); color: var(--muted); font-size: 11px; }
.button-primary, .dialog-button, .github-row button {
height: 40px; padding: 0 20px;
border: 0; border-radius: 11px;
color: #fff; background: var(--accent-grad);
font-weight: 600; letter-spacing: -.005em; cursor: pointer;
box-shadow: 0 8px 24px rgba(95,157,255,.32), inset 0 1px rgba(255,255,255,.28);
transition: transform .15s, box-shadow .15s, filter .15s;
}
.button-primary:hover, .dialog-button:hover, .github-row button:hover { filter: brightness(1.08); transform: translateY(-1px); box-shadow: 0 12px 30px rgba(95,157,255,.42), inset 0 1px rgba(255,255,255,.28); }
.button-primary:active, .dialog-button:active, .github-row button:active { transform: none; filter: brightness(.95); }
/* Inspector */
.inspector { min-width: 0; overflow: hidden; background: linear-gradient(180deg, var(--bg-raised), var(--bg)); border-left: 1px solid var(--line); }
.tabs { height: 46px; display: flex; align-items: center; gap: 2px; margin: 0 12px; padding: 0; border-bottom: 1px solid var(--line); }
.tab { position: relative; height: 46px; padding: 0 10px; border: 0; background: transparent; color: var(--dim); font-weight: 500; cursor: pointer; }
.tab:hover { color: var(--text); }
.tab.active { color: #fff; }
.tab.active::after { content: ""; position: absolute; left: 8px; right: 8px; bottom: -1px; height: 2px; border-radius: 2px; background: var(--accent-grad); box-shadow: 0 0 12px rgba(141,125,255,.7); }
#resultBadge:not(:empty) { margin-left: 4px; padding: 1px 6px; border-radius: 999px; background: var(--accent-soft); color: #cfc8ff; font-size: 10px; font-weight: 600; }
.panel { display: none; height: calc(100% - 46px); overflow: auto; padding: 16px 14px 24px; }
.panel.active { display: block; } .panel.active { display: block; }
.panel-hint, .result-summary { color: #888888; margin: 0 0 12px; } .panel-hint, .result-summary { margin: 0 0 14px; color: var(--muted); line-height: 1.6; }
.entity-title { margin-bottom: 12px; } .entity-title { margin-bottom: 14px; }
.entity-title small { display: block; color: #787878; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .entity-title small { display: block; color: var(--dim); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.entity-title h2 { margin: 6px 0 0; color: #ededed; font-size: 16px; font-weight: 550; letter-spacing: -.035em; overflow-wrap: anywhere; } .entity-title h2 { margin: 5px 0 0; color: #f4f6fb; font-size: 19px; font-weight: 650; letter-spacing: -.035em; overflow-wrap: anywhere; }
.meta-grid { display: grid; grid-template-columns: 1fr auto; gap: 5px 12px; padding: 10px 0; border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); } .meta-grid { display: grid; grid-template-columns: 1fr auto; gap: 0 12px; padding: 4px 12px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface-2); font-variant-numeric: tabular-nums; }
.meta-grid span { color: #888888; } .meta-grid span, .meta-grid b { padding: 7px 0; border-bottom: 1px solid var(--line); }
.meta-grid b { color: #d4d4d4; font-weight: 500; text-align: right; } .meta-grid span:nth-last-of-type(1), .meta-grid b:nth-last-of-type(1) { border-bottom: 0; }
.section-title { margin: 14px 0 7px; color: #e4e4e4; font-weight: 600; } .meta-grid span { color: var(--muted); }
.relation { display: flex; gap: 8px; align-items: center; padding: 5px 7px; margin: 0 -7px; border-radius: 4px; cursor: pointer; } .meta-grid b { color: var(--text); font-weight: 600; text-align: right; }
.relation:hover { background: #313131; } .section-title { margin: 20px 0 8px; color: var(--muted); font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; }
.relation i { width: 6px; height: 6px; border-radius: 50%; background: var(--green); } .relation { display: flex; gap: 9px; align-items: center; padding: 6px 8px; margin: 0 -8px; border-radius: 8px; cursor: pointer; }
.relation span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .relation:hover { background: var(--hover); }
.relation small { color: #777777; } .relation i { width: 6px; height: 6px; border-radius: 50%; background: var(--accent-2); box-shadow: 0 0 8px rgba(54,211,194,.6); }
.coverage-title { display: flex; align-items: baseline; justify-content: space-between; } .relation span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #cdd3df; }
.coverage-title small, .coverage-list-head small { color: #727272; font-size: 9px; font-weight: 400; } .relation small { color: var(--dim); font-size: 10px; }
.coverage-title, .coverage-list-head, .source-title { display: flex; align-items: baseline; justify-content: space-between; }
.coverage-title small, .coverage-list-head small, .source-title small { color: var(--dim); font-size: 10px; font-weight: 400; text-transform: none; letter-spacing: 0; }
.coverage-menu { display: grid; gap: 2px; } .coverage-menu { display: grid; gap: 2px; }
.coverage-row { width: 100%; min-height: 31px; display: grid; grid-template-columns: 7px 1fr auto 12px; gap: 8px; align-items: center; padding: 5px 7px; border: 1px solid transparent; border-radius: 5px; background: transparent; color: inherit; text-align: left; cursor: pointer; } .coverage-row { width: 100%; min-height: 34px; display: grid; grid-template-columns: 8px 1fr auto 12px; gap: 10px; align-items: center; padding: 5px 9px; border: 1px solid transparent; border-radius: 9px; background: transparent; color: #d6dbe6; text-align: left; cursor: pointer; }
.coverage-row:hover { background: #313131; } .coverage-row:hover { background: var(--hover); }
.coverage-row.active { border-color: #444444; background: #2e2e2e; } .coverage-row.active { border-color: var(--accent-line); background: var(--accent-soft); }
.coverage-row i { width: 7px; height: 7px; border-radius: 2px; } .coverage-row i { width: 8px; height: 8px; border-radius: 3px; box-shadow: 0 0 10px currentColor; }
.coverage-row span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .coverage-row span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.coverage-row small { color: #8e8e8e; font-variant-numeric: tabular-nums; } .coverage-row small { color: var(--dim); font-variant-numeric: tabular-nums; }
.coverage-row b { color: #767676; font-size: 13px; font-weight: 400; text-align: center; } .coverage-row b { color: var(--dim); font-size: 14px; font-weight: 400; text-align: center; }
.coverage-list-head { display: flex; justify-content: space-between; align-items: baseline; margin: 14px 0 6px; color: #dedede; font-weight: 600; } .coverage-list-head { margin: 18px 0 8px; color: var(--text); font-weight: 600; }
.coverage-file-list { position: relative; height: min(39vh, 350px); min-height: 150px; overflow: auto; border: 1px solid #3b3b3b; border-radius: 6px; outline: none; background: #222222; scrollbar-color: #545454 transparent; scrollbar-width: thin; } .coverage-file-list { position: relative; height: min(39vh, 350px); min-height: 150px; overflow: auto; border: 1px solid var(--line); border-radius: 12px; outline: none; background: rgba(0,0,0,.25); }
.coverage-file-list:focus { border-color: #808080; box-shadow: 0 0 0 2px rgba(75,145,204,.11); } .coverage-file-list:focus { border-color: var(--accent-line); box-shadow: 0 0 0 3px rgba(141,125,255,.14); }
.coverage-list-spacer { width: 1px; } .coverage-list-spacer { width: 1px; }
.coverage-list-window { position: absolute; inset: 0 0 auto; } .coverage-list-window { position: absolute; inset: 0 0 auto; }
.coverage-file { width: 100%; height: 43px; display: grid; grid-template-columns: 3px minmax(0,1fr) auto; gap: 8px; align-items: stretch; padding: 5px 7px; border: 0; border-bottom: 1px solid #2f2f2f; background: transparent; color: inherit; text-align: left; cursor: pointer; } .coverage-file { width: 100%; height: 43px; display: grid; grid-template-columns: 3px minmax(0,1fr) auto; gap: 10px; align-items: stretch; padding: 5px 10px; border: 0; border-bottom: 1px solid var(--line); background: transparent; color: inherit; text-align: left; cursor: pointer; }
.coverage-file:hover { background: #2b2b2b; } .coverage-file:hover { background: var(--hover); }
.coverage-file.active { background: #373737; box-shadow: inset 2px 0 #b2b2b2; } .coverage-file.active { background: var(--accent-soft); box-shadow: inset 2px 0 var(--accent); }
.coverage-file > i { border-radius: 2px; } .coverage-file > i { border-radius: 2px; }
.coverage-file > span { min-width: 0; align-self: center; } .coverage-file > span { min-width: 0; align-self: center; }
.coverage-file strong, .coverage-file small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .coverage-file strong, .coverage-file small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.coverage-file strong { color: #dedede; font-weight: 500; } .coverage-file strong { color: var(--text); font-weight: 500; }
.coverage-file small { color: #747474; font-size: 10px; } .coverage-file small { color: var(--dim); font-size: 10px; }
.coverage-file em { align-self: center; color: #7a7a7a; font-size: 9px; font-style: normal; font-variant-numeric: tabular-nums; } .coverage-file em { align-self: center; color: var(--dim); font-size: 10px; font-style: normal; font-variant-numeric: tabular-nums; }
.coverage-empty { padding: 18px 10px; color: #777777; text-align: center; } .coverage-empty { padding: 18px 10px; color: var(--dim); text-align: center; }
.source-title { display: flex; justify-content: space-between; align-items: baseline; } .code-preview { position: relative; height: min(46vh, 430px); min-height: 220px; overflow: auto; border: 1px solid var(--line); border-radius: 12px; outline: none; background: #06080c; color: #b6bfd0; font: 10.5px/16px var(--mono); tab-size: 2; }
.source-title small { color: #767676; font-weight: 400; } .code-preview:focus { border-color: var(--accent-line); box-shadow: 0 0 0 3px rgba(141,125,255,.12); }
.code-preview { position: relative; height: min(46vh,430px); min-height: 220px; overflow: auto; border: 1px solid #3a3a3a; border-radius: 3px; outline: none; background: #171717; color: #afafaf; font: 10px/16px "SFMono-Regular", Consolas, monospace; tab-size: 2; scrollbar-color: #545454 transparent; scrollbar-width: thin; }
.code-preview:focus { border-color: #626262; box-shadow: 0 0 0 1px rgba(70,154,123,.15); }
.source-spacer { position: relative; min-width: 100%; } .source-spacer { position: relative; min-width: 100%; }
.source-window { position: absolute; inset: 0 auto auto 0; min-width: 100%; margin: 0; padding: 0; color: inherit; font: inherit; line-height: 16px; white-space: pre; } .source-window { position: absolute; inset: 0 auto auto 0; min-width: 100%; margin: 0; padding: 6px 0 0; color: inherit; font: inherit; line-height: 16px; white-space: pre; }
.source-line { display: block; height: 16px; padding-right: 10px; } .source-line { display: block; height: 16px; padding-right: 12px; }
.source-line::before { content: attr(data-line); position: sticky; left: 0; display: inline-block; width: 35px; margin-right: 10px; padding-right: 7px; border-right: 1px solid #303030; background: #171717; color: #555555; text-align: right; user-select: none; } .source-line::before { content: attr(data-line); position: sticky; left: 0; display: inline-block; width: 40px; margin-right: 12px; padding-right: 8px; border-right: 1px solid var(--line); background: #06080c; color: #434b5c; text-align: right; user-select: none; }
.tok-plain { color: #aab5af; } .tok-plain { color: #b6bfd0; }
.tok-comment { color: #64726b; font-style: italic; } .tok-comment { color: #5c667a; font-style: italic; }
.tok-string { color: #89b88f; } .tok-string { color: #8fd694; }
.tok-number { color: #83a8d8; } .tok-number { color: #f5a97f; }
.tok-keyword { color: #d8a75b; } .tok-keyword { color: #c69cff; }
.tok-type { color: #79b8d8; } .tok-type { color: #6ec6ff; }
.tok-function { color: #d6cf8b; } .tok-function { color: #7aa7ff; }
.tok-property { color: #b397d5; } .tok-property { color: #5fd3c6; }
.tok-operator { color: #8d9c95; } .tok-operator { color: #8f9ab0; }
.tok-constant { color: #d98585; } .tok-constant { color: #ff8fa3; }
.tok-tag { color: #65b9aa; } .tok-tag { color: #5fd3c6; }
.result-row { display: grid; grid-template-columns: 3px 1fr; gap: 8px; padding: 7px 3px; border-bottom: 1px solid #313131; cursor: pointer; } .result-row { display: grid; grid-template-columns: 3px minmax(0,1fr); gap: 10px; min-width: 0; margin: 0 -6px; padding: 8px 8px; border-radius: 9px; cursor: pointer; }
.result-row:hover { background: #2c2c2c; } .result-row:hover { background: var(--hover); }
.result-row i { background: var(--yellow); border-radius: 2px; } .result-row i { border-radius: 2px; background: var(--yellow); }
.result-row strong { display: block; color: #dbdbdb; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .result-row strong { display: block; color: var(--text); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.result-row small { display: block; color: #777777; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .result-row small { display: block; color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.breadcrumb { position: absolute; left: 18px; bottom: 18px; max-width: calc(100% - 170px); display: flex; align-items: center; gap: 10px; padding: 7px 12px; border: 1px solid #414141; border-radius: 9px; background: #1c1c1ced; color: #ababab; box-shadow: 0 4px 16px #0002; } /* Floating canvas chrome */
.breadcrumb { position: absolute; left: 16px; bottom: 16px; max-width: calc(100% - 190px); display: flex; align-items: center; gap: 10px; padding: 8px 14px 8px 8px; border-radius: 12px; color: var(--muted); }
#breadcrumbText { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #breadcrumbText { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.breadcrumb button { border: 0; padding: 2px; background: transparent; cursor: pointer; color: #b2b2b2; } .breadcrumb button { width: 26px; height: 26px; display: grid; place-items: center; border: 1px solid var(--line); border-radius: 8px; padding: 0; background: var(--surface-2); color: var(--text); cursor: pointer; }
.fps { position: absolute; right: 22px; bottom: 65px; color: #a2a2a2; font: 10px monospace; } .breadcrumb button:hover { border-color: var(--accent-line); background: var(--accent-soft); }
.legend { position: absolute; right: 9px; top: 10px; display: none; width: 190px; padding: 6px; border: 1px solid #464646; border-radius: 6px; background: rgba(31,33,32,.94); box-shadow: 0 7px 25px rgba(0,0,0,.3); } .fps { position: absolute; right: 20px; bottom: 66px; padding: 3px 8px; border-radius: 6px; background: rgba(0,0,0,.45); color: var(--accent-2); font: 10px var(--mono); }
.legend.open { display: grid; gap: 2px; } .canvas-controls { position: absolute; right: 16px; bottom: 16px; display: flex; gap: 2px; padding: 4px; border-radius: 12px; }
.legend button { width: 100%; display: flex; justify-content: space-between; gap: 12px; padding: 5px 6px; border: 0; border-radius: 4px; color: inherit; background: transparent; text-align: left; cursor: pointer; } .canvas-controls button { display: grid; place-items: center; width: 32px; height: 30px; border: 0; border-radius: 8px; background: transparent; color: var(--muted); font-size: 18px; cursor: pointer; }
.legend button:hover { background: #343434; } .canvas-controls button:hover { background: var(--hover); color: #fff; }
.legend button b { color: #909090; font-weight: 500; } .canvas-controls svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; }
.legend i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; margin-right: 6px; } .legend { position: absolute; right: 12px; top: 12px; display: none; width: 210px; padding: 6px; border-radius: 12px; }
.tooltip { position: absolute; z-index: 3; max-width: 310px; padding: 7px 9px; pointer-events: none; border: 1px solid #515151; border-radius: 5px; background: rgba(27,29,28,.96); box-shadow: 0 5px 20px rgba(0,0,0,.35); color: #dedede; } .legend.open { display: grid; gap: 2px; animation: rise .18s ease-out; }
.camera-help { position: absolute; z-index: 2; left: 18px; top: 18px; width: fit-content; display: flex; gap: 13px; align-items: center; padding: 8px 12px; pointer-events: none; border: 1px solid rgba(110,118,113,.28); border-radius: 8px; background: rgba(27,29,28,.92); color: #a2a2a2; font-size: 10px; } .legend button { width: 100%; display: flex; justify-content: space-between; gap: 12px; padding: 7px 8px; border: 0; border-radius: 8px; background: transparent; color: var(--text); text-align: left; cursor: pointer; }
.camera-help[hidden] { display: none; } .legend button:hover { background: var(--hover); }
.camera-help { pointer-events: auto; flex-wrap: wrap; max-width: calc(100% - 36px); } .legend button b { color: var(--dim); font-weight: 500; font-variant-numeric: tabular-nums; }
.camera-help button { border: 1px solid #494949; border-radius: 5px; background: #292929; padding: 4px 7px; color: #ccc; cursor: pointer; font-size: 10px; } .legend i { display: inline-block; width: 8px; height: 8px; margin-right: 8px; border-radius: 3px; }
.camera-help button:hover { background: #414141; color: #fff; } .tooltip { position: absolute; z-index: 3; max-width: 320px; padding: 8px 11px; pointer-events: none; border-radius: 10px; color: #fff; font-weight: 500; }
.camera-help b { color: #b9b9b9; font-weight: 600; } .tooltip small { display: block; margin-top: 1px; color: var(--muted); font-weight: 400; }
.tooltip small { display: block; color: #888888; } .camera-help { position: absolute; z-index: 2; left: 16px; top: 16px; width: fit-content; max-width: calc(100% - 32px); display: flex; flex-wrap: wrap; gap: 12px; align-items: center; padding: 6px 12px 6px 6px; border-radius: 12px; color: var(--dim); font-size: 10.5px; }
.progress-card { position: absolute; z-index: 4; left: 50%; top: 22px; transform: translateX(-50%); width: min(420px, calc(100% - 30px)); padding: 12px 14px; border: 1px solid #474747; border-radius: 8px; background: rgba(37,40,38,.97); box-shadow: 0 12px 35px rgba(0,0,0,.34); } .camera-help button { padding: 5px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface-2); color: var(--text); font-size: 10.5px; cursor: pointer; }
.progress-copy { display: flex; justify-content: space-between; gap: 15px; } .camera-help button:hover { border-color: var(--accent-line); background: var(--accent-soft); }
.progress-copy span, .progress-count { color: #888888; } .camera-help b { color: var(--muted); font-weight: 600; }
.progress-track { height: 3px; margin: 10px 0 5px; overflow: hidden; background: #353535; border-radius: 2px; } @keyframes rise { from { opacity: 0; transform: translateY(4px); } }
.progress-track i { display: block; height: 100%; width: 5%; background: linear-gradient(90deg,#8b8b8b,#818181); transition: width .18s ease; }
.progress-count { font-size: 10px; }
.empty-state { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; gap: 4px; color: #c7c7c7; }
.empty-state span { color: #7a7a7a; }
dialog { width: min(560px, calc(100vw - 30px)); max-height: calc(100dvh - 32px); padding: 0; border: 1px solid #484848; border-radius: 18px; color: var(--text); background: linear-gradient(145deg,#2a2a2a,#1e1e1e); box-shadow: 0 32px 120px #0009, inset 0 1px #ffffff0c; } .progress-card { position: absolute; z-index: 4; left: 50%; top: 20px; width: min(440px, calc(100% - 30px)); padding: 18px 44px 16px 18px; border-radius: 16px; transform: translateX(-50%); animation: drop .25s ease-out; }
dialog::backdrop { background: rgba(8,10,9,.7); backdrop-filter: blur(5px); } @keyframes drop { from { opacity: 0; transform: translate(-50%, -8px); } }
.progress-copy { display: flex; flex-direction: column; gap: 3px; }
.progress-copy strong { color: #fff; font-size: 13.5px; font-weight: 600; letter-spacing: -.01em; }
.progress-copy span, .progress-count { color: var(--muted); }
#progressMessage { max-height: 100px; overflow: auto; overflow-wrap: anywhere; }
.progress-track { position: relative; height: 4px; margin: 13px 0 7px; overflow: hidden; border-radius: 999px; background: rgba(255,255,255,.07); }
.progress-track i { position: relative; display: block; height: 100%; width: 5%; overflow: hidden; border-radius: inherit; background: var(--accent-grad); box-shadow: 0 0 12px rgba(95,157,255,.6); transition: width .25s ease; }
.progress-track i::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, transparent, rgba(255,255,255,.55), transparent); transform: translateX(-100%); animation: shimmer 1.4s ease-in-out infinite; }
@keyframes shimmer { to { transform: translateX(100%); } }
.progress-count { font-size: 10.5px; font-variant-numeric: tabular-nums; }
.progress-dismiss { position: absolute; right: 8px; top: 8px; }
.progress-action { display: inline-grid; place-items: center; width: fit-content; height: 36px; margin-top: 14px; text-decoration: none; }
.empty-state { position: absolute; inset: 0; display: grid; place-content: center; gap: 4px; text-align: center; color: var(--text); }
.empty-state span { color: var(--muted); }
/* Dialogs */
dialog {
width: min(580px, calc(100vw - 30px));
max-height: calc(100dvh - 32px);
padding: 0;
border: 1px solid var(--line-strong);
border-radius: 22px;
color: var(--text);
background:
radial-gradient(600px 240px at 0% 0%, rgba(141,125,255,.16), transparent 70%),
radial-gradient(500px 260px at 100% 100%, rgba(54,211,194,.08), transparent 70%),
#0e1118;
box-shadow: 0 40px 140px rgba(0,0,0,.7), inset 0 1px rgba(255,255,255,.08);
}
dialog[open] { animation: pop .22s cubic-bezier(.2,.9,.3,1.2); }
@keyframes pop { from { opacity: 0; transform: scale(.97) translateY(6px); } }
dialog::backdrop { background: rgba(3,4,8,.62); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); }
.dialog-shell { padding: 30px; } .dialog-shell { padding: 30px; }
.dialog-head { display: flex; align-items: start; justify-content: space-between; margin-bottom: 17px; } .dialog-head { display: flex; align-items: start; justify-content: space-between; margin-bottom: 18px; }
.dialog-head small { color: #b6b6b6; font-size: 9px; font-weight: 600; letter-spacing: .18em; } .dialog-head small { background: var(--accent-grad); -webkit-background-clip: text; background-clip: text; color: transparent; font-size: 10px; font-weight: 700; letter-spacing: .2em; }
.dialog-head h1 { margin: 8px 0 0; font-size: 27px; font-weight: 550; letter-spacing: -.04em; } .dialog-head h1 { margin: 8px 0 0; font-size: 28px; font-weight: 700; letter-spacing: -.045em; color: #f4f6fb; }
.close-button { width: 30px; height: 30px; flex-shrink: 0; display: grid; place-items: center; padding: 0; border: 0; border-radius: 6px; background: transparent; color: #919191; cursor: pointer; } .dialog-description { margin: 8px 0 2px; color: var(--muted); font-size: 13px; }
.close-button svg { display: block; width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; } .close-button { width: 32px; height: 32px; flex-shrink: 0; display: grid; place-items: center; padding: 0; border: 1px solid transparent; border-radius: 10px; background: transparent; color: var(--dim); cursor: pointer; }
.close-button:hover { background: #313131; color: #fff; } .close-button svg { display: block; width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; }
.source-card { width: 100%; margin-top: 12px; padding: 18px; border: 1px solid #454545; border-radius: 12px; background: #ffffff03; color: inherit; text-align: left; } .close-button:hover { border-color: var(--line); background: var(--hover); color: #fff; }
button.source-card { display: flex; align-items: center; gap: 12px; cursor: pointer; } .source-card { width: 100%; margin-top: 12px; padding: 18px; border: 1px solid var(--line); border-radius: 16px; background: rgba(255,255,255,.025); color: inherit; text-align: left; transition: border-color .18s, background-color .18s, transform .18s, box-shadow .18s; }
button.source-card:hover { border-color: #787878; background: #2f2f2f; } button.source-card { display: flex; align-items: center; gap: 14px; cursor: pointer; }
.source-card > b { margin-left: auto; color: #828282; font-size: 17px; } button.source-card:hover { transform: translateY(-1px); border-color: var(--accent-line); background: rgba(141,125,255,.07); box-shadow: 0 10px 30px rgba(0,0,0,.3); }
.source-icon { width: 34px; height: 34px; display: grid; place-items: center; border: 1px solid #4d4d4d; border-radius: 7px; color: #a9a9a9; background: #242424; font-size: 18px; } .source-card:focus-within:not(button) { border-color: rgba(255,255,255,.14); }
.source-card > b { margin-left: auto; color: var(--dim); font-size: 18px; transition: transform .18s, color .18s; }
button.source-card:hover > b { transform: translateX(3px); color: #fff; }
.source-icon { width: 40px; height: 40px; display: grid; place-items: center; border: 1px solid rgba(141,125,255,.35); border-radius: 12px; color: #d9d2ff; background: linear-gradient(135deg, rgba(141,125,255,.22), rgba(54,211,194,.1)); }
.source-icon svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.source-card strong, .source-card small { display: block; } .source-card strong, .source-card small { display: block; }
.source-card small { margin-top: 2px; color: #808080; font-weight: 400; } .source-card strong { color: #f1f3f8; font-size: 13.5px; font-weight: 600; }
.github-card { display: grid; grid-template-columns: 34px 1fr; gap: 0 12px; } .source-card small { margin-top: 2px; color: var(--muted); font-weight: 400; }
.github-row { grid-column: 1 / -1; display: flex; gap: 7px; margin-top: 12px; } .github-card { display: grid; grid-template-columns: 40px 1fr; gap: 0 14px; align-items: center; }
.github-row input { flex: 1; min-width: 0; height: 34px; padding: 0 10px; border: 1px solid #474747; border-radius: 6px; outline: none; color: #e3e3e3; background: #212121; } .github-row { grid-column: 1 / -1; display: flex; gap: 8px; margin-top: 14px; }
.github-row input:focus { border-color: #979797; } .github-row input { flex: 1; min-width: 0; height: 42px; padding: 0 13px; border: 1px solid var(--line); border-radius: 11px; outline: none; color: var(--text); background: rgba(0,0,0,.35); font-family: var(--mono); font-size: 12px; transition: border-color .15s, box-shadow .15s; }
.github-row button { border: 1px solid #c7c7c7; border-radius: 7px; padding: 0 14px; color: #262626; background: #cdcdcd; cursor: pointer; font-weight: 600; } .github-row input::placeholder { color: var(--dim); }
.github-account { grid-column: 1 / -1; margin: 8px 0 0; color: #808080; font-size: 11px; } .github-row input:focus { border-color: var(--accent-line); box-shadow: 0 0 0 4px rgba(141,125,255,.14); }
.github-account a, .github-account button { padding: 0; border: 0; background: none; color: #cfcfcf; font: inherit; text-decoration: underline; cursor: pointer; } .github-row button { height: 42px; }
.progress-action { display: grid; place-items: center; width: fit-content; margin-top: 12px; text-decoration: none; } .github-account { grid-column: 1 / -1; margin: 10px 0 0; color: var(--muted); font-size: 11.5px; }
.form-error { grid-column: 1 / -1; min-height: 16px; margin: 5px 0 -5px; color: var(--danger); font-size: 11px; } .github-account a, .github-account button { padding: 0; border: 0; background: none; color: #cfc8ff; font: inherit; text-decoration: none; border-bottom: 1px solid rgba(207,200,255,.35); cursor: pointer; }
.dialog-foot { margin: 15px 2px 0; color: #707070; font-size: 10px; } .github-account a:hover, .github-account button:hover { color: #fff; border-bottom-color: #fff; }
.settings-dialog { width: min(460px, calc(100vw - 30px)); } .form-error { grid-column: 1 / -1; min-height: 16px; margin: 6px 0 -6px; color: var(--danger); font-size: 11px; }
.dialog-foot { display: flex; gap: 8px; align-items: center; margin: 16px 2px 0; color: var(--dim); font-size: 11px; line-height: 1.6; }
.dialog-foot::before { content: ""; flex-shrink: 0; width: 6px; height: 6px; border-radius: 50%; background: var(--green); box-shadow: 0 0 8px rgba(54,211,160,.7); }
.settings-dialog { width: min(480px, calc(100vw - 30px)); }
.settings-shell { padding: 28px; } .settings-shell { padding: 28px; }
.settings-status { display: flex; align-items: center; justify-content: space-between; margin: -3px 0 13px; padding: 9px 11px; border: 1px solid #404040; border-radius: 7px; background: #222222; } .settings-status { display: flex; align-items: center; justify-content: space-between; margin: -2px 0 8px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 12px; background: rgba(0,0,0,.25); }
.settings-status span { display: flex; align-items: center; gap: 7px; color: #dddddd; font-weight: 600; } .settings-status span { display: flex; align-items: center; gap: 8px; color: var(--text); font-weight: 600; }
.settings-status i { width: 7px; height: 7px; border-radius: 50%; background: var(--green); box-shadow: 0 0 0 3px rgba(41,201,151,.1); } .settings-status i { width: 7px; height: 7px; border-radius: 50%; background: var(--green); box-shadow: 0 0 8px rgba(54,211,160,.7); }
.settings-status small { color: #787878; font-size: 9px; letter-spacing: .07em; } .settings-status small { color: var(--dim); font-size: 10px; letter-spacing: .08em; }
.settings-grid { padding: 12px 2px; } .preference-row { display: flex; gap: 16px; justify-content: space-between; align-items: center; padding: 16px 0; border-bottom: 1px solid var(--line); }
.settings-note { display: grid; grid-template-columns: 32px 1fr; gap: 10px; align-items: start; margin-top: 14px; padding: 11px; border: 1px solid #383838; border-radius: 7px; background: #2a2a2a; } .preference-row label { color: var(--text); font-weight: 500; }
.settings-note-icon { width: 32px; height: 32px; display: grid; place-items: center; border-radius: 6px; color: #a3a3a3; background: #222222; font-size: 16px; } .preference-row small { display: block; margin-top: 3px; color: var(--muted); font-size: 11px; font-weight: 400; }
.preference-row select { padding: 8px 10px; border: 1px solid var(--line-strong); border-radius: 10px; background: rgba(0,0,0,.35); color: var(--text); }
.switch { appearance: none; flex-shrink: 0; width: 38px; height: 22px; margin: 0; padding: 2px; border: 1px solid var(--line-strong); border-radius: 999px; background: rgba(255,255,255,.06); cursor: pointer; transition: background .2s, border-color .2s; }
.switch::before { content: ""; display: block; width: 16px; height: 16px; border-radius: 50%; background: #aab2c3; transition: transform .2s cubic-bezier(.3,1.3,.5,1), background .2s; }
.switch:checked { border-color: transparent; background: var(--accent-grad); }
.switch:checked::before { background: #fff; transform: translateX(16px); }
.shortcut-guide { margin-top: 18px; color: var(--muted); font-size: 11px; }
.shortcut-guide > span { display: block; margin-bottom: 10px; }
.shortcut-guide div { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; }
.settings-note { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; margin-top: 16px; padding: 12px; border: 1px solid var(--line); border-radius: 14px; background: rgba(255,255,255,.025); }
.settings-note-icon { width: 34px; height: 34px; display: grid; place-items: center; border-radius: 10px; color: #d9d2ff; background: var(--accent-soft); font-size: 16px; }
.settings-note p { margin: 0; } .settings-note p { margin: 0; }
.settings-note strong, .settings-note small { display: block; } .settings-note strong, .settings-note small { display: block; }
.settings-note strong { color: #dbdbdb; font-size: 11px; } .settings-note strong { color: var(--text); font-size: 12px; }
.settings-note small { margin-top: 3px; color: #7a7a7a; line-height: 1.45; } .settings-note small { margin-top: 3px; color: var(--muted); line-height: 1.5; }
.settings-actions { display: flex; justify-content: flex-end; margin-top: 17px; } .settings-actions { display: flex; justify-content: flex-end; margin-top: 18px; }
.dialog-button { min-width: 84px; height: 36px; padding: 0 18px; border: 1px solid #c7c7c7; border-radius: 8px; color: #262626; background: #cdcdcd; cursor: pointer; font-weight: 600; } .dialog-button { min-width: 90px; }
.dialog-button:hover, .github-row button:hover { background: #dedede; }
.brand { display: flex; gap: 10px; align-items: center; padding-right: 11px; margin-right: 4px; border-right: 1px solid var(--line); color: #dbdbdb; font-size: 14px; font-weight: 600; letter-spacing: -.035em; line-height: 1.2; } button:focus-visible, a:focus-visible, input:focus-visible, select:focus-visible, [role="tab"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.brand svg { width: 21px; height: 21px; fill: #bbbbbb; }
.brand-sub { display: block; margin-top: 3px; font-size: 8px; letter-spacing: .25em; font-weight: 500; color: #999999; }
.view-switch, .metric-switch { padding: 2px; border: 1px solid #363636; border-radius: 10px; background: #15151566; gap: 3px; }
.filter-wrap { width: min(240px, 20vw); }
.filter-wrap input { height: 30px; border-radius: 9px; border-color: #3d3d3d; background: #181818; padding-left: 13px; }
.filter-wrap kbd { top: 6px; border: 1px solid #444444; border-radius: 3px; padding: 0 4px; }
.filter-wrap input:focus, .github-row input:focus { border-color: #a4a4a4; box-shadow: 0 0 0 3px #a4a4a415; }
.scene-stats { min-width: 0; margin-right: 8px; font-size: 11px; }
.scene-stats small { color: #8e8e8e; margin-top: 3px; }
.live-dot { flex-shrink: 0; margin-left: 5px; }
.panel-hint, .result-summary { color: #a2a2a2; line-height: 1.7; }
.meta-grid { gap: 5px 10px; padding: 10px 0; font-variant-numeric: tabular-nums; }
.meta-grid span, .entity-title small { color: #9a9a9a; }
.section-title { margin-top: 16px; }
.coverage-menu { gap: 2px; }
.coverage-row { min-height: 29px; border-radius: 6px; padding: 4px 7px; }
.coverage-row:hover, .relation:hover, .result-row:hover { background: #ffffff06; }
.coverage-row.active { background: #333333; border-color: #525252; }
.coverage-title small, .coverage-list-head small, .coverage-file small, .result-row small { color: #989898; }
.coverage-file-list { border-radius: 9px; border-color: #3f3f3f; background: #191919; }
.coverage-file.active { background: #343434; box-shadow: inset 2px 0 #bbbbbb; }
.coverage-file-list:focus { border-color: #aaaaaa; box-shadow: 0 0 0 2px #a4a4a415; }
.result-row { min-width: 0; border-radius: 7px; padding: 9px 8px; grid-template-columns: 3px minmax(0,1fr); }
.code-preview { border-radius: 9px; }
.canvas-controls { position: absolute; right: 18px; bottom: 18px; display: flex; padding: 4px; gap: 3px; border: 1px solid #414141; border-radius: 10px; background: #1c1c1cf5; box-shadow: 0 4px 16px #0002; }
.canvas-controls button { display: grid; place-items: center; width: 30px; height: 28px; background: transparent; color: #c9c9c9; border: 0; border-radius: 6px; font-size: 18px; cursor: pointer; }
.canvas-controls button:hover { background: #353535; }
.canvas-controls svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; }
.progress-card { border-radius: 12px; border-color: #4c4c4c; padding: 18px; padding-right: 40px; background: #262626f5; }
.progress-copy { flex-direction: column; gap: 4px; }
#progressMessage { overflow-wrap: anywhere; max-height: 100px; overflow: auto; }
.progress-dismiss { position: absolute; right: 5px; top: 5px; }
.dialog-description { margin: 9px 0 3px; color: #a3a3a3; font-size: 12px; }
.source-icon { width: 36px; height: 36px; border-color: #4c4c4c; background: #313131; color: #c5c5c5; }
.source-icon svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
.source-card small { color: #a1a1a1; }
.dialog-foot { color: #9a9a9a; line-height: 1.7; }
.github-row input { height: 39px; }
.settings-status { border-color: #414141; background: #1b1b1b; margin-bottom: 16px; }
.settings-note { background: #ffffff03; border-color: #3d3d3d; border-radius: 10px; }
.settings-note small { color: #a0a0a0; }
.preference-row { display: flex; gap: 16px; justify-content: space-between; align-items: center; padding: 16px 0; border-bottom: 1px solid var(--line-soft); }
.preference-row label { color: #dbdbdb; font-weight: 500; }
.preference-row small { display: block; font-size: 10px; font-weight: 400; color: #a0a0a0; margin-top: 3px; }
.preference-row select { padding: 7px 9px; border: 1px solid #4b4b4b; border-radius: 7px; background: #1e1e1e; color: #d0d0d0; font: inherit; }
.switch { appearance: none; flex-shrink: 0; width: 34px; height: 20px; border: 1px solid #575757; border-radius: 20px; background: #323232; cursor: pointer; margin: 0; padding: 2px; }
.switch::before { content: ''; display: block; width: 14px; height: 14px; border-radius: 50%; background: #9b9b9b; transition: transform .15s; }
.switch:checked { background: #b1b1b1; border-color: #b1b1b1; }
.switch:checked::before { background: #272727; transform: translateX(14px); }
.shortcut-guide { color: #a0a0a0; margin-top: 18px; font-size: 10px; }
.shortcut-guide > span { display: block; margin-bottom: 10px; }
.shortcut-guide div { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
kbd { font: 10px/1.5 ui-monospace, monospace; border: 1px solid #4b4b4b; border-radius: 4px; padding: 1px 5px; color: #d2d2d2; background: #1d1d1d; }
button:focus-visible, a:focus-visible, input:focus-visible, select:focus-visible, [role="tab"]:focus-visible { outline: 2px solid #bbbbbb; outline-offset: 3px; }
html[data-input-method="pointer"] button:focus { outline: none; } html[data-input-method="pointer"] button:focus { outline: none; }
button { transition: background-color .15s, border-color .15s, color .15s; }
button:active { filter: brightness(.93); }
::selection { background: #67676766; color: #f1f1f1; }
* { scrollbar-width: thin; scrollbar-color: #484848 transparent; }
@media (max-width: 1250px) { .scene-stats { display: none; } }
@media (max-width: 1250px) { .scene-stats { display: none; } }
@media (max-width: 900px) { @media (max-width: 900px) {
:root { --panel-width: 250px; } :root { --panel-width: 260px; }
.metric span, .scene-stats { display: none; } .metric span, .brand-name { display: none; }
.filter-wrap { width: 150px; } .brand { padding-right: 10px; }
.brand > span { display: none; } .filter-wrap { width: 160px; }
.brand { padding-right: 10px; margin-right: 0; }
.toolbar { gap: 4px; padding: 0 10px; } .toolbar { gap: 4px; padding: 0 10px; }
} }
@media (max-width: 670px) { @media (max-width: 670px) {
:root { --panel-width: 0px; --toolbar-height: 46px; } :root { --panel-width: 0px; }
.workspace { grid-template-columns: 1fr; grid-template-rows: minmax(160px,1fr) minmax(150px,28%); } .workspace { grid-template-columns: 1fr; grid-template-rows: minmax(160px,1fr) minmax(150px,30%); }
.inspector { display: block; border-left: 0; border-top: 1px solid var(--line); } .inspector { display: block; border-left: 0; border-top: 1px solid var(--line); }
.tabs { height: 36px; } .tabs { height: 40px; }
.panel { height: calc(100% - 36px); padding: 10px 12px; } .tab { height: 40px; }
.separator, #layersButton, #codeButton, .palette { display: none; } .panel { height: calc(100% - 40px); padding: 10px 12px; }
.filter-wrap { width: auto; flex: 1; min-width: 65px; } .separator, #layersButton, #codeButton, .palette, .brand, .metric-switch, #mapButton, .github-link, .toolbar-spacer { display: none; }
.brand, .metric-switch, #mapButton, .github-link, .toolbar-spacer { display: none; } .filter-wrap { flex: 1; width: auto; min-width: 65px; }
.tool { width: 30px; min-width: 30px; }
.dialog-shell { padding: 22px; } .dialog-shell { padding: 22px; }
.camera-help { gap: 8px; font-size: 9px; left: 10px; top: 10px; } .welcome { padding: 20px; }
.welcome-features { display: none; }
.camera-help { left: 10px; top: 10px; gap: 8px; font-size: 9.5px; }
.breadcrumb { left: 10px; bottom: 10px; } .breadcrumb { left: 10px; bottom: 10px; }
.canvas-controls { right: 10px; bottom: 10px; } .canvas-controls { right: 10px; bottom: 10px; }
} }