Redesign the interface as a hand-made dev zine

Replaces the gradient, frosted-glass look with flat warm ink, hard
black edges and offset shadows, a clashing lime/pink/sky/tangerine
palette, and self-hosted Archivo Black, Atkinson Hyperlegible and Space
Mono (Inter is gone). Stickers, slight tilts, a marker-style heading,
a barber-pole progress bar, wiggling icons and chattier copy give it
some restless energy; reduced-motion users get none of the animation.

Toolbar buttons are now labelled chips in consistent groups (Open;
2D/3D/City; Fit/Layers/Code/palette; Lines/Refs), and the city bar is
one even row: Heli, Walk, Fly, then Tour and Copy link with shortcuts,
help text underneath and the address moved to the top right. Copy link
feedback no longer wipes the button's icon.

A tab half way down the sidebar's left edge (or [) folds the sidebar
away and back, remembered per device. The local folder option is
hidden unless the server reports it enabled. Canvas text, posters and
signs switch to the new fonts once loaded, and the city's ground, fog,
grid, pavements and wall are warmed to match.
This commit is contained in:
Andy Dixon
2026-07-25 11:15:28 +01:00
parent 9299f3a9c4
commit cb8e525508
11 changed files with 397 additions and 434 deletions
+32 -14
View File
@@ -118,7 +118,7 @@ function computeLayout(){
function computeCity(){ function computeCity(){
cityModel=layoutCity(files,currentName); cityModel=layoutCity(files,currentName);
const instances=[{x:-80,y:-80,w:cityModel.width+160,h:cityModel.height+160,height:.02,color:[0,0,0],kind:KIND.ground}]; const instances=[{x:-80,y:-80,w:cityModel.width+160,h:cityModel.height+160,height:.02,color:[0,0,0],kind:KIND.ground}];
for(const block of cityModel.blocks)if(block.depth>0)instances.push({x:block.x,y:block.y,w:block.w,h:block.h,height:.1+.06*Math.min(block.depth,4),color:[.55,.6,.9],kind:KIND.block}); for(const block of cityModel.blocks)if(block.depth>0)instances.push({x:block.x,y:block.y,w:block.w,h:block.h,height:.1+.06*Math.min(block.depth,4),color:[.95,.9,.78],kind:KIND.block});
cityEntries=[]; cityEntries=[];
for(const item of layoutItems){ for(const item of layoutItems){
const building=cityModel.buildings.get(item.id);if(!building)continue; const building=cityModel.buildings.get(item.id);if(!building)continue;
@@ -140,7 +140,7 @@ function computeCity(){
cityFolders=cityModel.blocks.map(block=>({block,buildings:byDirectory.get(block.node.path)||[],label:folderLabel(block.node.path,currentName)})).filter(folder=>folder.buildings.length); cityFolders=cityModel.blocks.map(block=>({block,buildings:byDirectory.get(block.node.path)||[],label:folderLabel(block.node.path,currentName)})).filter(folder=>folder.buildings.length);
const tally=node=>{let files=node.files.length,lines=node.files.reduce((n,f)=>n+(f.lines||0),0);for(const child of node.children.values()){const t=tally(child);files+=t.files;lines+=t.lines;}return{files,lines};}; const tally=node=>{let files=node.files.length,lines=node.files.reduce((n,f)=>n+(f.lines||0),0);for(const child of node.children.values()){const t=tally(child);files+=t.files;lines+=t.lines;}return{files,lines};};
cityDistricts=cityModel.blocks.filter(block=>block.depth===1).map(block=>({block,...tally(block.node),top:Math.max(10,...[...cityIndex.near(block.x+block.w/2,block.y+block.h/2,Math.hypot(block.w,block.h)/2)].filter(b=>b.x>=block.x&&b.x+b.w<=block.x+block.w&&b.y>=block.y&&b.y+b.h<=block.y+block.h).map(b=>b.height))})); cityDistricts=cityModel.blocks.filter(block=>block.depth===1).map(block=>({block,...tally(block.node),top:Math.max(10,...[...cityIndex.near(block.x+block.w/2,block.y+block.h/2,Math.hypot(block.w,block.h)/2)].filter(b=>b.x>=block.x&&b.x+b.w<=block.x+block.w&&b.y>=block.y&&b.y+b.h<=block.y+block.h).map(b=>b.height))}));
for(const wall of cityWallBoxes)instances.push({...wall,color:[.55,.5,1],kind:KIND.wall}); for(const wall of cityWallBoxes)instances.push({...wall,color:[.78,.95,.21],kind:KIND.wall});
pastePosters(); pastePosters();
renderer.setCity(instances,{width:cityModel.width,height:cityModel.height}); renderer.setCity(instances,{width:cityModel.width,height:cityModel.height});
buildMinimap();updateCityHud();updateTrails();updateRoutes();updateBeacons();signState.key='';facadeState={entry:null,texture:null};renderer.setFacade(null); buildMinimap();updateCityHud();updateTrails();updateRoutes();updateBeacons();signState.key='';facadeState={entry:null,texture:null};renderer.setFacade(null);
@@ -324,8 +324,8 @@ $('#tourButton').addEventListener('click',event=>{event.currentTarget.blur();tou
// ---- Shareable places: repository, view, camera and file in the URL fragment ---- // ---- Shareable places: repository, view, camera and file in the URL fragment ----
let currentRepoUrl=''; let currentRepoUrl='';
$('#shareButton').addEventListener('click',async event=>{ $('#shareButton').addEventListener('click',async event=>{
const button=event.currentTarget,label=button.textContent;button.blur(); const button=event.currentTarget,labelElement=button.querySelector('.label'),label=labelElement.textContent;button.blur();
const flash=text=>{button.textContent=text;setTimeout(()=>{button.textContent=label;},1800);}; const flash=text=>{labelElement.textContent=text;button.classList.add('done');setTimeout(()=>{labelElement.textContent=label;button.classList.remove('done');},1800);};
if(!currentRepoUrl){flash('Needs a GitHub repo');return;} if(!currentRepoUrl){flash('Needs a GitHub repo');return;}
const hash=encodePlace({repo:currentRepoUrl,view:renderer.mode,camera:renderer.city,file:selected?.path}); const hash=encodePlace({repo:currentRepoUrl,view:renderer.mode,camera:renderer.city,file:selected?.path});
window.history.replaceState(null,'',hash); window.history.replaceState(null,'',hash);
@@ -372,8 +372,8 @@ function minimapTransform(){
function buildMinimap(){ function buildMinimap(){
const dpr=2,canvas=document.createElement('canvas');canvas.width=MINIMAP.width*dpr;canvas.height=MINIMAP.height*dpr; const dpr=2,canvas=document.createElement('canvas');canvas.width=MINIMAP.width*dpr;canvas.height=MINIMAP.height*dpr;
const ctx=canvas.getContext('2d'),{scale,ox,oy}=minimapTransform();ctx.scale(dpr,dpr); const ctx=canvas.getContext('2d'),{scale,ox,oy}=minimapTransform();ctx.scale(dpr,dpr);
ctx.strokeStyle='rgba(141,125,255,.55)';ctx.lineWidth=1.5;ctx.strokeRect(ox-WALL.margin*scale,oy-WALL.margin*scale,(cityModel.width+WALL.margin*2)*scale,(cityModel.height+WALL.margin*2)*scale); ctx.strokeStyle='rgba(200,241,53,.7)';ctx.lineWidth=1.5;ctx.strokeRect(ox-WALL.margin*scale,oy-WALL.margin*scale,(cityModel.width+WALL.margin*2)*scale,(cityModel.height+WALL.margin*2)*scale);
ctx.fillStyle='rgba(141,125,255,.08)'; ctx.fillStyle='rgba(243,236,220,.06)';
for(const block of cityModel.blocks)if(block.depth===1)ctx.fillRect(ox+block.x*scale,oy+block.y*scale,block.w*scale,block.h*scale); for(const block of cityModel.blocks)if(block.depth===1)ctx.fillRect(ox+block.x*scale,oy+block.y*scale,block.w*scale,block.h*scale);
for(const item of layoutItems){const b=item.city;if(!b||!fileAlerts.has(item.path))continue;ctx.fillStyle='#ff4d3d';ctx.beginPath();ctx.arc(ox+(b.x+b.w/2)*scale,oy+(b.y+b.h/2)*scale,2.6,0,Math.PI*2);ctx.fill();} for(const item of layoutItems){const b=item.city;if(!b||!fileAlerts.has(item.path))continue;ctx.fillStyle='#ff4d3d';ctx.beginPath();ctx.arc(ox+(b.x+b.w/2)*scale,oy+(b.y+b.h/2)*scale,2.6,0,Math.PI*2);ctx.fill();}
for(const item of layoutItems){const b=item.city;if(!b)continue;ctx.fillStyle=`rgba(${item.color.map(v=>Math.round(v*255)).join(',')},.85)`;ctx.fillRect(ox+b.x*scale,oy+b.y*scale,Math.max(.6,b.w*scale),Math.max(.6,b.h*scale));} for(const item of layoutItems){const b=item.city;if(!b)continue;ctx.fillStyle=`rgba(${item.color.map(v=>Math.round(v*255)).join(',')},.85)`;ctx.fillRect(ox+b.x*scale,oy+b.y*scale,Math.max(.6,b.w*scale),Math.max(.6,b.h*scale));}
@@ -491,7 +491,7 @@ function escapeHtml(value=''){return String(value).replace(/[&<>"]/g,char=>({'&'
const syntaxPalette={plain:'#b6bfd0',comment:'#5c667a',string:'#8fd694',number:'#f5a97f',keyword:'#c69cff',type:'#6ec6ff',function:'#7aa7ff',property:'#5fd3c6',operator:'#8f9ab0',constant:'#ff8fa3',tag:'#5fd3c6'}; 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. // Matches the renderer's tile top so code drawn over a 3D roof blends with the WebGL face.
const TILE_TOP='#0e1118'; const TILE_TOP='#16140f';
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;
@@ -605,9 +605,9 @@ 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 GitHub repository to begin.';content.innerHTML='';return;} if(!files.length){hint.hidden=false;hint.textContent='Open a codebase to get going.';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':'Click anything on the map. Go on.';
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('');
const expandedLabel=layerLabels[expandedCoverageLayer]; const expandedLabel=layerLabels[expandedCoverageLayer];
content.innerHTML=`<div class="entity-title"><small>Coverage · ${escapeHtml(currentName)}</small><h2>${format(files.length)} indexed files</h2></div><div class="meta-grid"><span>known links</span><b>${format(sceneStats.known)}</b><span>inferred links</span><b>${format(sceneStats.inferred)}</b><span>definitions</span><b>${format(sceneStats.definitions)}</b><span>source lines</span><b>${format(sceneStats.totalLines)}</b><span>security alerts</span><b>${alertSummary()}</b></div><div class="section-title coverage-title"><span>Semantic coverage</span><small>Click a section</small></div><div class="coverage-menu">${coverageRows}</div>${expandedCoverageLayer?`<div class="coverage-list-head"><span>${escapeHtml(expandedLabel)}</span><small>Click or ↑ ↓ to navigate</small></div><div class="coverage-file-list" id="coverageFileList" tabindex="0" role="listbox" aria-label="${escapeHtml(expandedLabel)} files"><div class="coverage-list-spacer" id="coverageListSpacer"></div><div class="coverage-list-window" id="coverageListWindow"></div></div>`:''}`; content.innerHTML=`<div class="entity-title"><small>Coverage · ${escapeHtml(currentName)}</small><h2>${format(files.length)} indexed files</h2></div><div class="meta-grid"><span>known links</span><b>${format(sceneStats.known)}</b><span>inferred links</span><b>${format(sceneStats.inferred)}</b><span>definitions</span><b>${format(sceneStats.definitions)}</b><span>source lines</span><b>${format(sceneStats.totalLines)}</b><span>security alerts</span><b>${alertSummary()}</b></div><div class="section-title coverage-title"><span>Semantic coverage</span><small>Click a section</small></div><div class="coverage-menu">${coverageRows}</div>${expandedCoverageLayer?`<div class="coverage-list-head"><span>${escapeHtml(expandedLabel)}</span><small>Click or ↑ ↓ to navigate</small></div><div class="coverage-file-list" id="coverageFileList" tabindex="0" role="listbox" aria-label="${escapeHtml(expandedLabel)} files"><div class="coverage-list-spacer" id="coverageListSpacer"></div><div class="coverage-list-window" id="coverageListWindow"></div></div>`:''}`;
@@ -696,7 +696,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 Inter, system-ui, sans-serif'; ctx.font='600 11px "Space Mono", ui-monospace, monospace';
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);
@@ -704,7 +704,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(10,12,18,.88)';ctx.fillRect(x,y,width,17); ctx.fillStyle='rgba(21,19,15,.92)';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;
@@ -716,7 +716,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 Inter, system-ui, 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 "Space Mono", ui-monospace, monospace';ctx.textBaseline='top';
codeTexturesPending=false; codeTexturesPending=false;
codeTextureDeadline=performance.now()+5; codeTextureDeadline=performance.now()+5;
if(renderer.mode==='city'){visibleScreenItems=[];drawCityOverlay();ctx.restore();return;} if(renderer.mode==='city'){visibleScreenItems=[];drawCityOverlay();ctx.restore();return;}
@@ -1242,6 +1242,7 @@ window.addEventListener('resize',()=>{dirty=true;});
const dialog=$('#loadDialog');$('#loadButton').addEventListener('click',()=>dialog.showModal());$('#welcomeOpen').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()=>{
if(!localFoldersEnabled)return;
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);}}
else $('#folderFallback').click(); else $('#folderFallback').click();
@@ -1323,12 +1324,17 @@ async function resumeAfterGithub(){
requestAnimationFrame(()=>(pending&&tone==='ok'?$('#githubButton'):$('#githubInput')).focus()); requestAnimationFrame(()=>(pending&&tone==='ok'?$('#githubButton'):$('#githubInput')).focus());
} }
let localFoldersEnabled=false;
$$('.local-only').forEach(element=>{element.hidden=true;});
async function checkBackend(){ async function checkBackend(){
const indicator=$('.live-dot'); const indicator=$('.live-dot');
try{ try{
const response=await apiFetch(apiUrl('/api/health')); const response=await apiFetch(apiUrl('/api/health'));
if(!response.ok)throw new Error(`HTTP ${response.status}`); if(!response.ok)throw new Error(`HTTP ${response.status}`);
const health=await response.json();
indicator.classList.add('connected');indicator.title='Indexer connected';indicator.setAttribute('aria-label','Indexer connected'); indicator.classList.add('connected');indicator.title='Indexer connected';indicator.setAttribute('aria-label','Indexer connected');
// Local folder uploads only exist when the server was started with SHOW_LOCAL=true.
localFoldersEnabled=health.localFolders===true;$$('.local-only').forEach(element=>{element.hidden=!localFoldersEnabled;});
}catch{ }catch{
indicator.classList.remove('connected');indicator.title='Indexer unavailable';indicator.setAttribute('aria-label','Indexer unavailable'); indicator.classList.remove('connected');indicator.title='Indexer unavailable';indicator.setAttribute('aria-label','Indexer unavailable');
} }
@@ -1360,6 +1366,18 @@ dialog.addEventListener('click',event=>{if(event.target===dialog)dialog.close();
$$('.tab').forEach((tab,index)=>{tab.id=`tab-${tab.dataset.tab}`;tab.setAttribute('aria-controls',`panel-${tab.dataset.tab}`);$(`#panel-${tab.dataset.tab}`).setAttribute('aria-labelledby',tab.id);tab.addEventListener('keydown',event=>{if(!['ArrowLeft','ArrowRight','Home','End'].includes(event.key))return;event.preventDefault();const tabs=$$('.tab'),next=event.key==='Home'?0:event.key==='End'?tabs.length-1:(index+(event.key==='ArrowRight'?1:-1)+tabs.length)%tabs.length;switchTab(tabs[next].dataset.tab);tabs[next].focus();});}); $$('.tab').forEach((tab,index)=>{tab.id=`tab-${tab.dataset.tab}`;tab.setAttribute('aria-controls',`panel-${tab.dataset.tab}`);$(`#panel-${tab.dataset.tab}`).setAttribute('aria-labelledby',tab.id);tab.addEventListener('keydown',event=>{if(!['ArrowLeft','ArrowRight','Home','End'].includes(event.key))return;event.preventDefault();const tabs=$$('.tab'),next=event.key==='Home'?0:event.key==='End'?tabs.length-1:(index+(event.key==='ArrowRight'?1:-1)+tabs.length)%tabs.length;switchTab(tabs[next].dataset.tab);tabs[next].focus();});});
switchTab('inspector'); switchTab('inspector');
new ResizeObserver(()=>{dirty=true;}).observe(viewport); new ResizeObserver(()=>{dirty=true;}).observe(viewport);
$('#settingsButton').title='Workspace settings';$('#settingsButton').setAttribute('aria-label','Workspace settings'); // Sidebar folds away with the tab on its left edge (or [ ); remembered on this device.
$('#settingsButton svg').innerHTML='<circle cx="12" cy="12" r="3"/><path d="m9.5 3-.5 2-2 1-2-.5L3 9l1.5 1.5v3L3 15l2 3.5 2-.5 2 1 .5 2h5l.5-2 2-1 2 .5 2-3.5-1.5-1.5v-3L21 9l-2-3.5-2 .5-2-1-.5-2z"/>'; function setInspectorHidden(hidden){
$('.workspace').classList.toggle('inspector-hidden',hidden);
const toggle=$('#inspectorToggle');toggle.setAttribute('aria-expanded',String(!hidden));toggle.title=hidden?'Show the sidebar ([)':'Hide the sidebar ([)';
try{localStorage.setItem('codenav.inspectorHidden',hidden?'1':'');}catch{}
dirty=true;
}
$('#inspectorToggle').addEventListener('click',event=>{event.currentTarget.blur();setInspectorHidden(!$('.workspace').classList.contains('inspector-hidden'));});
document.addEventListener('keydown',event=>{if(event.key==='['&&!event.metaKey&&!event.ctrlKey&&!/INPUT|TEXTAREA|SELECT/.test(document.activeElement?.tagName)&&!document.querySelector('dialog[open]')){event.preventDefault();$('#inspectorToggle').click();}});
try{if(localStorage.getItem('codenav.inspectorHidden'))setInspectorHidden(true);}catch{}
// Canvas text (signs, posters, labels) needs the web fonts loaded before it rasterises them.
Promise.all(['400 30px "Archivo Black"','400 13px "Atkinson Hyperlegible"','700 13px "Atkinson Hyperlegible"','700 11px "Space Mono"'].map(font=>document.fonts.load(font))).then(()=>{
labelAtlas.clear();if(cityModel)pastePosters();signState.key='';resetCodeCaches();dirty=true;
}).catch(()=>{});
renderer.setData([]);fitScene();renderInspector();renderHistory();requestAnimationFrame(animate);checkBackend();loadGithubStatus().then(()=>/[?&](github|setup_action)=/.test(location.search)?resumeAfterGithub():openPlaceFromHash()); renderer.setData([]);fitScene();renderInspector();renderHistory();requestAnimationFrame(animate);checkBackend();loadGithubStatus().then(()=>/[?&](github|setup_action)=/.test(location.search)?resumeAfterGithub():openPlaceFromHash());
+6 -4
View File
@@ -1,6 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="14" fill="#171918"/> <rect width="64" height="64" rx="12" fill="#1e1b16"/>
<path d="M10 13h44v38H10z" fill="#202b28" stroke="#65b3ff" stroke-width="3"/> <g stroke="#000" stroke-width="3" transform="rotate(-6 32 32)">
<path d="M10 29h44M29 13v38M29 40h25" fill="none" stroke="#29c997" stroke-width="3"/> <rect x="8" y="24" width="15" height="32" rx="2" fill="#c8f135"/>
<path d="M15 19h9M15 23h7M34 19h14M34 23h10M15 35h9M15 39h7M34 46h13" fill="none" stroke="#d6dad7" stroke-linecap="round" stroke-width="2" opacity=".82"/> <rect x="25" y="8" width="15" height="48" rx="2" fill="#ff5ea8"/>
<rect x="42" y="32" width="15" height="24" rx="2" fill="#5cc8ff"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 449 B

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+41 -41
View File
@@ -3,46 +3,45 @@
<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="#0b0d13"> <meta name="theme-color" content="#1e1b16">
<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="preload" href="./fonts/archivo-black.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="./fonts/atkinson-400.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"><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="brand" aria-label="CodeNavigator"><svg viewBox="0 0 28 28" aria-hidden="true"><rect x="2.5" y="10" width="7" height="15" rx="1" fill="#c8f135" stroke="#000" stroke-width="1.6"/><rect x="10.5" y="3" width="7" height="22" rx="1" fill="#ff5ea8" stroke="#000" stroke-width="1.6"/><rect x="18.5" y="14" width="7" height="11" rx="1" fill="#5cc8ff" stroke="#000" stroke-width="1.6"/></svg><span class="brand-name">Code<em>Navigator</em></span></div>
<div class="tool-group"> <button class="tool primary" id="loadButton" title="Open a codebase (Ctrl+O)"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3.5 7.5h6l2-2h4l2 2h3v11h-17z"/><path d="M3.5 9.5h17"/></svg><span class="label">Open</span></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> <div class="tool-group" role="group" aria-label="View mode">
<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" data-view="2d" title="Flat map"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="3.5" width="17" height="17" rx="1.5"/><path d="M3.5 11h17M12 3.5v17"/></svg><span class="label">2D</span></button>
<button class="tool" id="layersButton" title="Semantic layers" aria-label="Semantic layers"><svg viewBox="0 0 24 24"><path d="m4 8 8-4 8 4-8 4zM4 12l8 4 8-4M4 16l8 4 8-4"/></svg></button> <button class="tool" data-view="3d" title="3D landscape"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m12 3 8 4.5v9L12 21l-8-4.5v-9zM4 7.5l8 4.5 8-4.5M12 12v9"/></svg><span class="label">3D</span></button>
<button class="tool" data-view="city" title="Walkable city"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 21h18M5 21V10l4-2v13M9 21V4l6 3v14M15 21v-9l4-1.5V21"/></svg><span class="label">City</span></button>
</div> </div>
<span class="separator"></span> <div class="tool-group" role="group" aria-label="Map options">
<div class="tool-group view-switch" role="group" aria-label="View mode"> <button class="tool" id="mapButton" title="Fit everything in view (F)"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4H4v4m12-4h4v4M4 16v4h4m12-4v4h-4"/></svg><span class="label">Fit</span></button>
<button class="tool active" data-view="2d" title="Flat map" aria-label="Flat map"><svg viewBox="0 0 24 24"><rect x="3.5" y="3.5" width="17" height="17" rx="1.5"/><path d="M3.5 11h17M12 3.5v17"/></svg></button> <button class="tool" id="layersButton" title="Semantic layers"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m4 8 8-4 8 4-8 4zM4 12l8 4 8-4M4 16l8 4 8-4"/></svg><span class="label">Layers</span></button>
<button class="tool" data-view="3d" title="3D landscape" aria-label="3D landscape"><svg viewBox="0 0 24 24"><path d="m12 3 8 4.5v9L12 21l-8-4.5v-9zM4 7.5l8 4.5 8-4.5M12 12v9"/></svg></button> <button class="tool" id="codeButton" title="Show source code on tiles"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m8 7-5 5 5 5M16 7l5 5-5 5M14 4l-4 16"/></svg><span class="label">Code</span></button>
<button class="tool" data-view="city" title="City" aria-label="City"><svg viewBox="0 0 24 24"><path d="M3 21h18M5 21V10l4-2v13M9 21V4l6 3v14M15 21v-9l4-1.5V21"/><path d="M11.5 9v.01M11.5 12v.01M11.5 15v.01"/></svg></button> <button class="tool icon-only" id="paletteButton" title="Change palette" aria-label="Change palette"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 13a8 8 0 1 0-8 7h1.5a1.5 1.5 0 0 0 0-3H12a2 2 0 0 1 0-4z"/><circle cx="7.5" cy="11" r="1"/><circle cx="10" cy="7.5" r="1"/><circle cx="14.5" cy="7.5" r="1"/></svg></button>
<button class="tool" id="codeButton" title="Toggle source texture" aria-label="Toggle source texture"><svg viewBox="0 0 24 24"><path d="M4 18 9 6l4 8 2-5 5 9z"/><path d="M4 20h16"/></svg></button>
</div> </div>
<span class="separator"></span> <div class="tool-group metric-switch" role="group" aria-label="Map metric">
<div class="metric-switch" role="group" aria-label="Map metric"> <button class="metric active" data-metric="lines" title="Size tiles by lines"><b>#</b> <span>Lines</span></button>
<button class="metric active" data-metric="lines"><b>#</b> <span>Lines</span></button> <button class="metric" data-metric="references" title="Size tiles by references"><b>⌁</b> <span>Refs</span></button>
<button class="metric" data-metric="references"><b>⌁</b> <span>References</span></button>
</div> </div>
<button class="tool palette" id="paletteButton" title="Change palette" aria-label="Change palette"><svg viewBox="0 0 24 24"><path d="M20 13a8 8 0 1 0-8 7h1.5a1.5 1.5 0 0 0 0-3H12a2 2 0 0 1 0-4z"/><circle cx="7.5" cy="11" r="1"/><circle cx="10" cy="7.5" r="1"/><circle cx="14.5" cy="7.5" r="1"/><circle cx="17" cy="11" r="1"/></svg></button>
<div class="toolbar-spacer"></div> <div class="toolbar-spacer"></div>
<div class="scene-stats" id="sceneStats" aria-live="polite"> <div class="scene-stats" id="sceneStats" aria-live="polite">
<span id="headlineStats">No codebase loaded</span> <span id="headlineStats">no repo yet</span>
<small id="fileStats">Open a folder or public repository</small> <small id="fileStats">nothing open yet</small>
</div> </div>
<label class="filter-wrap"> <label class="filter-wrap">
<span class="sr-only">Filter files and symbols</span> <span class="sr-only">Filter files and symbols</span>
<input id="searchInput" type="search" placeholder="Find a file or symbol…" autocomplete="off" spellcheck="false"> <input id="searchInput" type="search" placeholder="find a file or symbol" autocomplete="off" spellcheck="false">
<kbd>/</kbd> <kbd>/</kbd>
</label> </label>
<button class="tool" id="settingsButton" title="Performance details" aria-label="Performance details" aria-haspopup="dialog" aria-controls="settingsDialog"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19 13.5v-3l-2-.7-.7-1.7.9-1.9-2.1-2.1-1.9.9-1.7-.7-.7-2h-3l-.7 2-1.7.7-1.9-.9-2.1 2.1.9 1.9-.7 1.7-2 .7v3l2 .7.7 1.7-.9 1.9 2.1 2.1 1.9-.9 1.7.7.7 2h3l.7-2 1.7-.7 1.9.9 2.1-2.1-.9-1.9.7-1.7z"/></svg></button> <button class="tool icon-only" id="settingsButton" title="Settings" aria-label="Settings" aria-haspopup="dialog" aria-controls="settingsDialog"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="m9.5 3-.5 2-2 1-2-.5L3 9l1.5 1.5v3L3 15l2 3.5 2-.5 2 1 .5 2h5l.5-2 2-1 2 .5 2-3.5-1.5-1.5v-3L21 9l-2-3.5-2 .5-2-1-.5-2z"/></svg></button>
<span class="live-dot" role="status" aria-label="Connecting to indexer" title="Connecting to indexer"></span> <span class="live-dot" role="status" aria-label="Connecting to indexer" title="Connecting to indexer"></span>
</header> </header>
@@ -51,15 +50,16 @@
<canvas id="glCanvas"></canvas> <canvas id="glCanvas"></canvas>
<canvas id="overlayCanvas"></canvas> <canvas id="overlayCanvas"></canvas>
<div class="welcome" id="welcome"> <div class="welcome" id="welcome">
<span class="eyebrow"><i></i>Codebase landscapes</span> <span class="eyebrow"><i></i>code, but it's a place now</span>
<h1>See your code<br><span>as a place.</span></h1> <h1>See your code<br><span>as a city.</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-sticker" aria-hidden="true">now with fire<small>(security alerts)</small></div>
<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> <p>Every file is a building, every folder a street. <b>Fly over it, walk around it, get lost in it.</b> Search every symbol, follow imports door to door, and watch the insecure bits smoulder.</p>
<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 class="welcome-actions"><button type="button" class="button-primary" id="welcomeOpen">Open a codebase →</button><span>or hit <kbd>Ctrl</kbd> <kbd>O</kbd></span></div>
<div class="welcome-features"><span>public + private GitHub</span><span class="local-only">local folders</span><span>walkable cities</span><span>import routes</span><span>things on fire</span><span>little people</span></div>
</div> </div>
<div class="empty-state" id="emptyState" hidden> <div class="empty-state" id="emptyState" hidden>
<strong>No source files found</strong> <strong>Nothing to see here</strong>
<span>Try another folder or public GitHub repository.</span> <span>No source files in there. Try another repo?</span>
</div> </div>
<div class="progress-card" id="progressCard" role="status" aria-live="polite" hidden> <div class="progress-card" id="progressCard" role="status" aria-live="polite" hidden>
<button type="button" class="progress-dismiss close-button" id="dismissProgress" aria-label="Dismiss notification"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg></button> <button type="button" class="progress-dismiss close-button" id="dismissProgress" aria-label="Dismiss notification"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg></button>
@@ -82,14 +82,13 @@
<span><b>F</b> reset</span> <span><b>F</b> reset</span>
</div> </div>
<div class="city-hud" id="cityHud" hidden> <div class="city-hud" id="cityHud" hidden>
<div class="city-views" role="group" aria-label="City camera"> <div class="city-row" role="group" aria-label="City camera">
<button type="button" data-city-view="heli" title="Helicopter (1)"><kbd>1</kbd> Helicopter</button> <button type="button" class="chip" data-city-view="heli" title="Helicopter (1)"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 6h18M12 6v3M6 13a6 4 0 0 0 12 0 6 4 0 0 0-12 0zM18 13h3M9 17l-2 3M15 17l2 3"/></svg><span class="label">Heli</span><kbd>1</kbd></button>
<button type="button" data-city-view="walk" title="Walk (2)"><kbd>2</kbd> Walk</button> <button type="button" class="chip" data-city-view="walk" title="Walk (2)"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="13" cy="4" r="2"/><path d="m9 21 2-6 3 2v4M8 11l3-3 3 1 2 3M11 8l-1 6"/></svg><span class="label">Walk</span><kbd>2</kbd></button>
<button type="button" data-city-view="fly" title="Fly (3)"><kbd>3</kbd> Fly</button> <button type="button" class="chip" data-city-view="fly" title="Fly (3)"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2 16 22 8l-9 11-2-5z"/><path d="m11 14 11-6"/></svg><span class="label">Fly</span><kbd>3</kbd></button>
</div> <span class="divider" aria-hidden="true"></span>
<div class="city-actions"> <button type="button" class="chip" id="tourButton" title="Guided tour (T)"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21s-7-6.5-7-12a7 7 0 0 1 14 0c0 5.5-7 12-7 12z"/><circle cx="12" cy="9" r="2.5"/></svg><span class="label">Tour</span><kbd>T</kbd></button>
<button type="button" id="tourButton" title="Guided tour (T)">Tour</button> <button type="button" class="chip" id="shareButton" title="Copy a link to this exact spot"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"/></svg><span class="label">Copy link</span></button>
<button type="button" id="shareButton" title="Copy a link to this place">Copy link</button>
</div> </div>
<span class="city-help" id="cityHelp"></span> <span class="city-help" id="cityHelp"></span>
</div> </div>
@@ -101,13 +100,14 @@
<canvas class="minimap" id="minimap" width="220" height="150" hidden title="Click to travel"></canvas> <canvas class="minimap" id="minimap" width="220" height="150" hidden title="Click to travel"></canvas>
<nav class="breadcrumb" id="breadcrumb" aria-label="Selected file path"> <nav class="breadcrumb" id="breadcrumb" aria-label="Selected file path">
<button id="homeButton" aria-label="Fit entire codebase">←</button> <button id="homeButton" aria-label="Fit entire codebase">←</button>
<span id="breadcrumbText">No codebase loaded</span> <span id="breadcrumbText">nothing open</span>
</nav> </nav>
<div class="fps" id="fps">— fps</div> <div class="fps" id="fps">— fps</div>
<div class="legend" id="legend"></div> <div class="legend" id="legend"></div>
<div class="canvas-controls" aria-label="Landscape navigation"><button type="button" id="zoomOutButton" title="Zoom out" aria-label="Zoom out">−</button><button type="button" id="fitButton" title="Fit codebase (F)" aria-label="Fit codebase"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4H4v4m12-4h4v4M4 16v4h4m12-4v4h-4"/></svg></button><button type="button" id="zoomInButton" title="Zoom in" aria-label="Zoom in">+</button></div> <div class="canvas-controls" aria-label="Landscape navigation"><button type="button" id="zoomOutButton" title="Zoom out" aria-label="Zoom out">−</button><button type="button" id="fitButton" title="Fit codebase (F)" aria-label="Fit codebase"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4H4v4m12-4h4v4M4 16v4h4m12-4v4h-4"/></svg></button><button type="button" id="zoomInButton" title="Zoom in" aria-label="Zoom in">+</button></div>
</div> </div>
<button type="button" class="inspector-toggle" id="inspectorToggle" title="Hide the sidebar ([)" aria-controls="inspector" aria-expanded="true"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m9 5 7 7-7 7"/></svg></button>
<aside class="inspector" id="inspector"> <aside class="inspector" id="inspector">
<div class="tabs" role="tablist"> <div class="tabs" role="tablist">
<button class="tab active" data-tab="inspector" role="tab">Inspector</button> <button class="tab active" data-tab="inspector" role="tab">Inspector</button>
@@ -133,11 +133,11 @@
<dialog id="loadDialog" aria-labelledby="loadTitle"> <dialog id="loadDialog" aria-labelledby="loadTitle">
<form method="dialog" class="dialog-shell"> <form method="dialog" class="dialog-shell">
<div class="dialog-head"> <div class="dialog-head">
<div><small>YOUR NEXT PERSPECTIVE</small><h1 id="loadTitle">Open a codebase</h1><p class="dialog-description">See the big picture. Explore every detail.</p></div> <div><small>LET'S GO</small><h1 id="loadTitle">Open a codebase</h1><p class="dialog-description">Big repos welcome. Weird ones too.</p></div>
<button class="close-button" value="cancel" aria-label="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg></button> <button class="close-button" value="cancel" aria-label="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg></button>
</div> </div>
<p class="github-notice" id="githubNotice" role="status" hidden></p> <p class="github-notice" id="githubNotice" role="status" hidden></p>
<button type="button" class="source-card" id="localFolderButton"> <button type="button" class="source-card local-only" id="localFolderButton">
<span class="source-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 7h7l2-2h4l2 2h3v12H3zM3 10h18"/></svg></span> <span class="source-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 7h7l2-2h4l2 2h3v12H3zM3 10h18"/></svg></span>
<span><strong>Local folder</strong><small>Temporarily indexed by this server</small></span> <span><strong>Local folder</strong><small>Temporarily indexed by this server</small></span>
<b>→</b> <b>→</b>
@@ -156,7 +156,7 @@
<dialog id="settingsDialog" class="settings-dialog" aria-labelledby="settingsTitle"> <dialog id="settingsDialog" class="settings-dialog" aria-labelledby="settingsTitle">
<form method="dialog" class="dialog-shell settings-shell"> <form method="dialog" class="dialog-shell settings-shell">
<div class="dialog-head"> <div class="dialog-head">
<div><small>MAKE IT YOURS</small><h1 id="settingsTitle">Workspace settings</h1><p class="dialog-description">A workspace that feels right for you.</p></div> <div><small>FIDDLE WITH STUFF</small><h1 id="settingsTitle">Settings</h1><p class="dialog-description">Tweak it until it feels right.</p></div>
<button class="close-button" value="cancel" aria-label="Close settings"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg></button> <button class="close-button" value="cancel" aria-label="Close settings"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg></button>
</div> </div>
<div id="settingsContent"></div> <div id="settingsContent"></div>
+8 -8
View File
@@ -17,15 +17,15 @@ export class ShelfPacker {
const STYLES = { const STYLES = {
// Name plates on roofs and walls. // Name plates on roofs and walls.
plate: { font: '600 30px Inter, system-ui, sans-serif', text: '#f1f3f8', fill: 'rgba(10,12,18,.9)', border: 'rgba(255,255,255,.14)' }, plate: { font: '700 30px "Atkinson Hyperlegible", system-ui, sans-serif', text: '#f1f3f8', fill: 'rgba(10,12,18,.9)', border: 'rgba(255,255,255,.14)' },
selected: { font: '700 30px Inter, system-ui, sans-serif', text: '#1a1405', fill: 'rgba(255,236,140,.96)', border: 'rgba(255,255,255,.5)' }, selected: { font: '700 30px "Atkinson Hyperlegible", system-ui, sans-serif', text: '#1a1405', fill: 'rgba(255,236,140,.96)', border: 'rgba(255,255,255,.5)' },
// Folder blades at street corners, in street-sign green. // Folder blades at street corners, in street-sign green.
street: { font: '600 30px Inter, system-ui, sans-serif', text: '#f4fff9', fill: 'rgba(14,112,76,.96)', border: 'rgba(255,255,255,.55)' }, street: { font: '700 30px "Atkinson Hyperlegible", system-ui, sans-serif', text: '#f4fff9', fill: 'rgba(14,112,76,.96)', border: 'rgba(255,255,255,.55)' },
// Hazard tape: square-cut, edge to edge, with warning stripes where the repeated label joins. // Hazard tape: square-cut, edge to edge, with warning stripes where the repeated label joins.
tapeDependabot: { font: '800 28px Inter, system-ui, sans-serif', text: '#141414', fill: '#f5c518', stripe: '#141414', tape: true }, tapeDependabot: { font: '400 28px "Archivo Black", Impact, sans-serif', text: '#141414', fill: '#f5c518', stripe: '#141414', tape: true },
tapeCodeScanning: { font: '800 28px Inter, system-ui, sans-serif', text: '#ffffff', fill: '#d7263d', stripe: '#ffffff', tape: true }, tapeCodeScanning: { font: '400 28px "Archivo Black", Impact, sans-serif', text: '#ffffff', fill: '#d7263d', stripe: '#ffffff', tape: true },
// District and street names painted on the ground. // District and street names painted on the ground.
ground: { font: '700 34px Inter, system-ui, sans-serif', text: 'rgba(205,198,255,.9)' }, ground: { font: '400 34px "Archivo Black", Impact, sans-serif', text: 'rgba(205,198,255,.9)' },
}; };
export class LabelAtlas { export class LabelAtlas {
@@ -166,9 +166,9 @@ function paintPoster(ctx, { title, body }, w, h, theme, variant) {
const shape = variant % 4; const shape = variant % 4;
if (shape === 0) { ctx.beginPath(); ctx.arc(w / 2, 100, 50, 0, Math.PI * 2); ctx.fill(); } if (shape === 0) { ctx.beginPath(); ctx.arc(w / 2, 100, 50, 0, Math.PI * 2); ctx.fill(); }
else if (shape === 1) { for (let i = 0; i < 8; i++) { const x = inset + i * 44; ctx.beginPath(); ctx.moveTo(x, inset); ctx.lineTo(x + 22, inset); ctx.lineTo(x - 30, 150); ctx.lineTo(x - 52, 150); ctx.fill(); } } else if (shape === 1) { for (let i = 0; i < 8; i++) { const x = inset + i * 44; ctx.beginPath(); ctx.moveTo(x, inset); ctx.lineTo(x + 22, inset); ctx.lineTo(x - 30, 150); ctx.lineTo(x - 52, 150); ctx.fill(); } }
else if (shape === 2) { ctx.beginPath(); ctx.moveTo(w / 2, 40); ctx.lineTo(w / 2 + 64, 160); ctx.lineTo(w / 2 - 64, 160); ctx.closePath(); ctx.fill(); ctx.fillStyle = theme.paper; ctx.font = '900 64px Inter, system-ui, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'alphabetic'; ctx.fillText('!', w / 2, 150); } else if (shape === 2) { ctx.beginPath(); ctx.moveTo(w / 2, 40); ctx.lineTo(w / 2 + 64, 160); ctx.lineTo(w / 2 - 64, 160); ctx.closePath(); ctx.fill(); ctx.fillStyle = theme.paper; ctx.font = '400 64px "Archivo Black", Impact, sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'alphabetic'; ctx.fillText('!', w / 2, 150); }
else { for (const y of [40, 84, 128]) ctx.fillRect(inset, y, pw, 22); } else { for (const y of [40, 84, 128]) ctx.fillRect(inset, y, pw, 22); }
const font = (weight, size) => `${weight} ${size}px Inter, system-ui, sans-serif`; const font = (weight, size) => weight >= 900 ? `400 ${size}px "Archivo Black", Impact, sans-serif` : `${weight >= 600 ? 700 : 400} ${size}px "Atkinson Hyperlegible", system-ui, sans-serif`;
const measure = weight => (text, size) => { ctx.font = font(weight, size); return ctx.measureText(text).width; }; const measure = weight => (text, size) => { ctx.font = font(weight, size); return ctx.measureText(text).width; };
const footer = 30, textTop = 176, textWidth = pw - pad * 2, textBottom = h - inset - footer; const footer = 30, textTop = 176, textWidth = pw - pad * 2, textBottom = h - inset - footer;
const titleFit = fitText(measure(900), title.toUpperCase(), textWidth, (textBottom - textTop) * .55, 34, 14, 3); const titleFit = fitText(measure(900), title.toUpperCase(), textWidth, (textBottom - textTop) * .55, 34, 14, 3);
+5 -5
View File
@@ -107,7 +107,7 @@ void main(){
outColor = vec4(vColor.rgb * pow(1.0 - vUnit.z, 1.6) * .55, 0.0); outColor = vec4(vColor.rgb * pow(1.0 - vUnit.z, 1.6) * .55, 0.0);
return; return;
} }
if(vKind.x == 2){ outColor = vec4(fog(vec3(.03, .036, .052)), uAlpha); return; } // asphalt if(vKind.x == 2){ outColor = vec4(fog(vec3(.035, .032, .028)), uAlpha); return; } // asphalt
if(vKind.x == 4){ if(vKind.x == 4){
// City wall: concrete courses and panel joints in metres, lit coping along the top. // City wall: concrete courses and panel joints in metres, lit coping along the top.
vec2 faceUv = vFace == 0 || vFace == 5 ? vUnit.xy : vFace < 3 ? vUnit.xz : vUnit.yz; vec2 faceUv = vFace == 0 || vFace == 5 ? vUnit.xy : vFace < 3 ? vUnit.xz : vUnit.yz;
@@ -117,7 +117,7 @@ void main(){
if(vFace == 0) concrete = vec3(.2, .21, .26); if(vFace == 0) concrete = vec3(.2, .21, .26);
float edgeWall = min(min(faceUv.x, 1.0 - faceUv.x), min(faceUv.y, 1.0 - faceUv.y)); float edgeWall = min(min(faceUv.x, 1.0 - faceUv.x), min(faceUv.y, 1.0 - faceUv.y));
float rim = 1.0 - smoothstep(0.0, fwidth(edgeWall) * 1.5 + .002, edgeWall); float rim = 1.0 - smoothstep(0.0, fwidth(edgeWall) * 1.5 + .002, edgeWall);
outColor = vec4(fog(mix(concrete, vec3(.55, .5, 1.0) * .8, rim * step(.9, vUnit.z + (vFace == 0 ? 1.0 : 0.0)))), uAlpha); outColor = vec4(fog(mix(concrete, vec3(.78, .95, .21) * .8, rim * step(.9, vUnit.z + (vFace == 0 ? 1.0 : 0.0)))), uAlpha);
return; return;
} }
#endif #endif
@@ -126,7 +126,7 @@ void main(){
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);
// Tile tops match TILE_TOP in app.js so source drawn over a roof blends in; 2D gets a faint layer tint. // Tile tops match TILE_TOP in app.js so source drawn over a roof blends in; 2D gets a faint layer tint.
vec3 top = vec3(.055, .067, .094); vec3 top = vec3(.086, .078, .059);
vec3 base = uMode < .5 ? mix(top, vColor.rgb, .07) : top; vec3 base = uMode < .5 ? mix(top, vColor.rgb, .07) : top;
#ifdef CITY #ifdef CITY
vec3 stroke = mix(vColor.rgb, vec3(1.0), .3) * 1.2; // neon edges at night vec3 stroke = mix(vColor.rgb, vec3(1.0), .3) * 1.2; // neon edges at night
@@ -173,7 +173,7 @@ void main(){ gl_Position = projectWorld(vec3(aPoint, 0.0)); }`;
const GRID_FS = `#version 300 es const GRID_FS = `#version 300 es
precision highp float; precision highp float;
out vec4 outColor; out vec4 outColor;
void main(){ outColor = vec4(.59, .63, 1.0, .11); }`; void main(){ outColor = vec4(.95, .92, .86, .08); }`;
// Textured quads for city signs and facades: centre, half-width axis, half-height axis, atlas rect. // Textured quads for city signs and facades: centre, half-width axis, half-height axis, atlas rect.
const SIGN_VS = `#version 300 es const SIGN_VS = `#version 300 es
@@ -726,7 +726,7 @@ export class LandscapeRenderer {
render(alpha = 1) { render(alpha = 1) {
const gl = this.gl; this.resize(); const gl = this.gl; this.resize();
const fogColor = [.027, .031, .05], fog = this.fogDensity(); const fogColor = [.05, .045, .038], fog = this.fogDensity();
if (this.mode === 'city') gl.clearColor(...fogColor, 1); else gl.clearColor(0, 0, 0, 0); if (this.mode === 'city') gl.clearColor(...fogColor, 1); else gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 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);
+305 -362
View File
@@ -1,438 +1,381 @@
@font-face { /* CodeNavigator: a hand-cut dev zine. Flat ink, hard edges, stickers, a little chaos. */
font-family: "Inter"; @font-face { font-family: "Archivo Black"; font-weight: 400; font-display: swap; src: url("./fonts/archivo-black.woff2") format("woff2"); }
font-style: normal; @font-face { font-family: "Atkinson Hyperlegible"; font-weight: 400; font-display: swap; src: url("./fonts/atkinson-400.woff2") format("woff2"); }
font-weight: 400 700; @font-face { font-family: "Atkinson Hyperlegible"; font-weight: 700; font-display: swap; src: url("./fonts/atkinson-700.woff2") format("woff2"); }
font-display: swap; @font-face { font-family: "Space Mono"; font-weight: 400; font-display: swap; src: url("./fonts/space-mono-400.woff2") format("woff2"); }
src: url("./inter-latin.woff2") format("woff2"); @font-face { font-family: "Space Mono"; font-weight: 700; font-display: swap; src: url("./fonts/space-mono-700.woff2") format("woff2"); }
}
:root { :root {
color-scheme: dark; color-scheme: dark;
--bg: #07090d; --ink: #15130f;
--bg-raised: #0d1017; --ink-2: #1e1b16;
--surface: rgba(16, 19, 27, .74); --ink-3: #2a261f;
--surface-solid: #10131b; --black: #000;
--surface-2: rgba(255, 255, 255, .035); --paper: #f3ecdc;
--hover: rgba(255, 255, 255, .06); --muted: #bdb39d;
--line: rgba(255, 255, 255, .08); --dim: #867c67;
--line-strong: rgba(255, 255, 255, .15); --line: #3b352b;
--text: #e9ecf3; --lime: #c8f135;
--muted: #9ba3b4; --pink: #ff5ea8;
--dim: #687083; --sky: #5cc8ff;
--accent: #8d7dff; --tang: #ff9a1f;
--accent-2: #36d3c2; --red: #ff4d3d;
--accent-grad: linear-gradient(135deg, #9a7dff 0%, #5f9dff 50%, #36d3c2 100%); --yellow: #ffd23f;
--accent-soft: rgba(141, 125, 255, .16); --danger: #ff4d3d;
--accent-line: rgba(141, 125, 255, .5); --hard: 3px 3px 0 var(--black);
--green: #36d3a0; --hard-lg: 5px 5px 0 var(--black);
--yellow: #f2c94c; --display: "Archivo Black", Impact, "Arial Black", sans-serif;
--danger: #ff6b7a; --body: "Atkinson Hyperlegible", system-ui, sans-serif;
--glass: blur(18px) saturate(150%); --mono: "Space Mono", ui-monospace, Consolas, monospace;
--shadow: 0 18px 50px rgba(0, 0, 0, .45), 0 2px 8px rgba(0, 0, 0, .3); --panel-width: 310px;
--panel-width: 300px; --toolbar-height: 56px;
--toolbar-height: 52px;
--mono: "JetBrains Mono", "SFMono-Regular", ui-monospace, Consolas, monospace;
} }
* { box-sizing: border-box; } * { box-sizing: border-box; }
[hidden] { display: none !important; } [hidden] { display: none !important; }
html, body, #app { width: 100%; height: 100%; overflow: hidden; } html, body, #app { width: 100%; height: 100%; overflow: hidden; }
body { body { margin: 0; background: var(--ink); color: var(--paper); font: 13px/1.5 var(--body); -webkit-font-smoothing: antialiased; }
margin: 0;
background: var(--bg);
color: var(--text);
font: 12.5px/1.5 Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
font-feature-settings: "cv11", "ss01";
-webkit-font-smoothing: antialiased;
}
button, input, select { 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; } ::selection { background: var(--pink); color: var(--black); }
* { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.14) transparent; } * { scrollbar-width: thin; scrollbar-color: var(--line) 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); } kbd { display: inline-block; min-width: 18px; padding: 0 4px; border: 2px solid var(--black); border-bottom-width: 3px; border-radius: 4px; background: var(--paper); color: var(--black); font: 700 10px/1.4 var(--mono); text-align: center; }
/* Toolbar */ @keyframes wiggle { 25% { transform: rotate(-9deg); } 75% { transform: rotate(7deg); } }
.toolbar { @keyframes blink { 50% { opacity: .35; } }
position: relative; @keyframes barber { to { background-position: 28px 0; } }
z-index: 5; @keyframes plonk { from { opacity: 0; transform: translateY(-10px) rotate(-3deg); } }
height: var(--toolbar-height); @keyframes bob { 50% { transform: rotate(4deg) translateY(-4px); } }
padding: 0 14px;
display: flex; /* ---- Toolbar ---- */
align-items: center; .toolbar { position: relative; z-index: 5; height: var(--toolbar-height); padding: 0 14px; display: flex; align-items: center; gap: 10px; background: var(--ink-2); border-bottom: 2px solid var(--black); }
gap: 8px; .toolbar::after { content: ""; position: absolute; left: 0; right: 0; bottom: -6px; height: 4px; background: repeating-linear-gradient(90deg, var(--lime) 0 38px, var(--pink) 38px 61px, var(--sky) 61px 87px, var(--tang) 87px 101px); }
background: linear-gradient(180deg, rgba(18, 21, 30, .96), rgba(11, 13, 19, .96)); .brand { display: flex; gap: 9px; align-items: center; padding-right: 14px; border-right: 2px dashed var(--line); }
border-bottom: 1px solid var(--line); .brand svg { width: 28px; height: 28px; transform: rotate(-6deg); }
} .brand-name { display: flex; align-items: center; gap: 4px; font: 400 15px/1 var(--display); color: var(--paper); }
.toolbar::after { .brand-name em { font-style: normal; padding: 3px 6px 2px; border: 2px solid var(--black); border-radius: 4px; background: var(--lime); color: var(--black); box-shadow: 2px 2px 0 var(--black); transform: rotate(-3deg); }
content: "";
position: absolute; /* Toolbar buttons are labelled chips: icon + word, same height everywhere. */
left: 0; right: 0; bottom: -1px; .tool-group { display: flex; align-items: center; gap: 4px; padding: 3px; border: 2px solid var(--black); border-radius: 10px; background: var(--ink); }
height: 1px; .tool, .metric { height: 32px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 10px; border: 2px solid transparent; border-radius: 7px; background: transparent; color: var(--muted); font: 700 11px var(--mono); text-transform: uppercase; letter-spacing: .03em; white-space: nowrap; cursor: pointer; transition: background-color .12s, color .12s, transform .12s, box-shadow .12s; }
background: linear-gradient(90deg, transparent, rgba(141,125,255,.55) 30%, rgba(54,211,194,.45) 70%, transparent); .tool.icon-only { width: 36px; padding: 0; }
opacity: .55; .tool svg { width: 16px; height: 16px; flex-shrink: 0; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
} .tool:hover svg { animation: wiggle .35s ease-in-out; }
.brand { display: flex; gap: 10px; align-items: center; padding-right: 14px; margin-right: 2px; border-right: 1px solid var(--line); } .metric b { font: 700 13px/1 var(--mono); }
.brand svg { width: 26px; height: 26px; filter: drop-shadow(0 0 10px rgba(141,125,255,.45)); } .tool:hover, .metric:hover { color: var(--paper); background: var(--ink-3); }
.brand-name { font-size: 15px; font-weight: 650; letter-spacing: -.03em; line-height: 1; color: #f2f4f9; } .tool.active, .metric.active { color: var(--black); border-color: var(--black); background: var(--lime); box-shadow: 2px 2px 0 var(--black); }
.brand-name em { font-style: normal; background: var(--accent-grad); -webkit-background-clip: text; background-clip: text; color: transparent; } .metric.active { background: var(--pink); }
.tool-group { display: flex; align-items: center; gap: 3px; } .tool.primary { color: var(--black); border-color: var(--black); background: var(--tang); box-shadow: 3px 3px 0 var(--black); transform: rotate(-2deg); }
.tool, .metric { .tool.primary:hover { background: var(--yellow); transform: rotate(1deg) translate(-1px, -1px); box-shadow: 4px 4px 0 var(--black); }
height: 30px;
min-width: 30px;
border: 1px solid transparent;
border-radius: 9px;
background: transparent;
color: var(--muted);
cursor: pointer;
transition: background-color .15s, border-color .15s, color .15s, box-shadow .15s;
}
.tool { width: 30px; padding: 0; display: grid; place-items: center; }
.tool svg { width: 16px; height: 16px; display: block; fill: none; stroke: currentColor; stroke-width: 1.65; stroke-linecap: round; stroke-linejoin: round; }
.metric { padding: 0 10px; display: flex; gap: 6px; align-items: center; font-weight: 500; }
.metric b { font: 500 13px/1 var(--mono); opacity: .8; }
.tool:hover, .metric:hover { color: var(--text); background: var(--hover); }
.tool.active, .metric.active {
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%; }
.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; }
.separator { width: 1px; height: 24px; background: var(--line); margin: 0 3px; }
.toolbar-spacer { flex: 1; } .toolbar-spacer { flex: 1; }
.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 { white-space: nowrap; text-align: right; line-height: 1.3; font: 700 11px var(--mono); color: var(--paper); font-variant-numeric: tabular-nums; }
.scene-stats small { display: block; margin-top: 3px; color: var(--dim); font-weight: 400; } .scene-stats small { display: block; color: var(--dim); font-weight: 400; }
.filter-wrap { position: relative; width: min(260px, 22vw); } .filter-wrap { position: relative; flex-shrink: 0; width: clamp(200px, 18vw, 270px); }
.filter-wrap::before { .filter-wrap::before { content: "?"; position: absolute; left: 9px; top: 50%; width: 18px; height: 18px; display: grid; place-items: center; border: 2px solid var(--black); border-radius: 50%; background: var(--sky); color: var(--black); font: 700 10px/1 var(--mono); transform: translateY(-50%) rotate(-10deg); pointer-events: none; }
content: ""; .filter-wrap input { width: 100%; height: 38px; padding: 0 36px 0 36px; border: 2px solid var(--black); border-radius: 8px; outline: none; background: var(--ink); color: var(--paper); box-shadow: var(--hard); transition: border-color .12s, box-shadow .12s; }
position: absolute; left: 11px; top: 50%; .filter-wrap input::placeholder { color: var(--dim); font-style: italic; }
width: 12px; height: 12px; transform: translateY(-50%); .filter-wrap input:focus { border-color: var(--pink); box-shadow: 3px 3px 0 var(--pink); }
background: currentColor; color: var(--dim); .filter-wrap kbd { position: absolute; right: 9px; top: 50%; transform: translateY(-50%) rotate(4deg); }
-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; .live-dot { flex-shrink: 0; width: 12px; height: 12px; border: 2px solid var(--black); border-radius: 50%; background: var(--dim); }
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; .live-dot.connected { background: var(--lime); animation: blink 1.6s steps(2, jump-none) infinite; }
pointer-events: none;
}
.filter-wrap input {
width: 100%; height: 34px; padding: 0 34px 0 31px;
border: 1px solid var(--line); border-radius: 10px;
outline: none; color: var(--text); background: rgba(0,0,0,.3);
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 ---- */
.workspace { position: absolute; inset: var(--toolbar-height) 0 0; display: grid; grid-template-columns: minmax(0,1fr) var(--panel-width); } .workspace { position: absolute; inset: calc(var(--toolbar-height) + 4px) 0 0; display: grid; grid-template-columns: minmax(0,1fr) var(--panel-width); transition: grid-template-columns .22s ease; }
.viewport { .workspace.inspector-hidden { grid-template-columns: minmax(0,1fr) 0px; }
position: relative; min-width: 0; overflow: hidden; touch-action: none; user-select: none; cursor: grab; .workspace.inspector-hidden .inspector { border-left: 0; }
background: .viewport { position: relative; min-width: 0; overflow: hidden; touch-action: none; user-select: none; cursor: grab; background: radial-gradient(circle at 1px 1px, rgba(243,236,220,.07) 1.2px, transparent 0) 0 0 / 24px 24px, var(--ink); }
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; }
.breadcrumb, .canvas-controls, .camera-help, .legend, .tooltip, .progress-card, .city-hud, .city-target, .city-address, .minimap, .tour-caption { /* A tab stuck to the sidebar's left edge, half way down, that folds the sidebar away. */
border: 1px solid var(--line); .inspector-toggle { position: absolute; z-index: 6; top: 50%; right: var(--panel-width); width: 22px; height: 64px; margin-right: -1px; display: grid; place-items: center; padding: 0; border: 2px solid var(--black); border-right: 0; border-radius: 10px 0 0 10px; background: var(--lime); color: var(--black); box-shadow: -3px 3px 0 var(--black); transform: translateY(-50%); transition: right .22s ease, background-color .12s, width .12s; cursor: pointer; }
background: var(--surface); .inspector-toggle:hover { width: 28px; background: var(--yellow); }
-webkit-backdrop-filter: var(--glass); .inspector-toggle svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; transition: transform .22s; }
backdrop-filter: var(--glass); .workspace.inspector-hidden .inspector-toggle { right: 0; }
box-shadow: var(--shadow), inset 0 1px rgba(255,255,255,.05); .workspace.inspector-hidden .inspector-toggle svg { transform: rotate(180deg); }
}
/* Welcome */ /* Floating things on the map are stickers: flat, black-edged, hard shadow, never quite square. */
.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; } .breadcrumb, .canvas-controls, .camera-help, .legend, .tooltip, .progress-card, .city-hud, .city-target, .city-address, .minimap, .tour-caption { border: 2px solid var(--black); background: var(--ink-2); box-shadow: var(--hard); }
.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 */ /* ---- Welcome ---- */
.inspector { min-width: 0; overflow: hidden; background: linear-gradient(180deg, var(--bg-raised), var(--bg)); border-left: 1px solid var(--line); } .welcome { position: absolute; inset: 0; z-index: 1; display: grid; place-content: center; justify-items: start; max-width: 780px; margin: 0 auto; padding: 32px; cursor: default; }
.tabs { height: 46px; display: flex; align-items: center; gap: 2px; margin: 0 12px; padding: 0; border-bottom: 1px solid var(--line); } .welcome .eyebrow { display: inline-flex; align-items: center; gap: 8px; padding: 5px 11px; border: 2px solid var(--black); border-radius: 4px; background: var(--pink); color: var(--black); box-shadow: 2px 2px 0 var(--black); font: 700 11px var(--mono); text-transform: uppercase; letter-spacing: .06em; transform: rotate(-3deg); }
.tab { position: relative; height: 46px; padding: 0 10px; border: 0; background: transparent; color: var(--dim); font-weight: 500; cursor: pointer; } .welcome .eyebrow i { width: 8px; height: 8px; border-radius: 50%; background: var(--black); animation: blink 1s steps(2, jump-none) infinite; }
.tab:hover { color: var(--text); } .welcome h1 { position: relative; margin: 22px 0 0; font: 400 clamp(38px, 6vw, 72px)/.98 var(--display); color: var(--paper); }
.tab.active { color: #fff; } .welcome h1 span { display: inline-block; margin-top: 8px; padding: .02em .16em .1em; border: 3px solid var(--black); border-radius: 6px; background: var(--lime); color: var(--black); box-shadow: 5px 5px 0 var(--black); transform: rotate(-2deg); }
.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); } .welcome p { max-width: 540px; margin: 20px 0 0; color: var(--muted); font-size: 16px; line-height: 1.6; }
#resultBadge:not(:empty) { margin-left: 4px; padding: 1px 6px; border-radius: 999px; background: var(--accent-soft); color: #cfc8ff; font-size: 10px; font-weight: 600; } .welcome p b { color: var(--paper); }
.panel { display: none; height: calc(100% - 46px); overflow: auto; padding: 16px 14px 24px; } .welcome-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; margin-top: 30px; color: var(--dim); font: 11px var(--mono); }
.welcome-features { display: flex; flex-wrap: wrap; gap: 9px; margin-top: 38px; }
.welcome-features span { padding: 5px 10px; border: 2px solid var(--line); border-radius: 999px; color: var(--muted); font: 700 11px var(--mono); }
.welcome-features span:nth-child(4n+1) { border-color: var(--lime); transform: rotate(-2deg); }
.welcome-features span:nth-child(4n+2) { border-color: var(--sky); transform: rotate(1.5deg); }
.welcome-features span:nth-child(4n+3) { border-color: var(--pink); transform: rotate(-1deg); }
.welcome-features span:nth-child(4n) { border-color: var(--tang); transform: rotate(2.5deg); }
.welcome-sticker { position: absolute; right: 0; top: 16%; width: 100px; height: 100px; display: grid; place-content: center; border: 2px solid var(--black); border-radius: 50%; background: var(--tang); color: var(--black); box-shadow: var(--hard); text-align: center; font: 400 13px/1.05 var(--display); text-transform: uppercase; transform: rotate(12deg); animation: bob 3.2s ease-in-out infinite; }
.welcome-sticker small { display: block; margin-top: 2px; font: 700 9px var(--mono); text-transform: none; }
.button-primary, .dialog-button, .github-row button { height: 42px; padding: 0 20px; border: 2px solid var(--black); border-radius: 8px; background: var(--lime); color: var(--black); box-shadow: 4px 4px 0 var(--black); font: 400 14px var(--display); cursor: pointer; transition: transform .1s, box-shadow .1s, background-color .1s; }
.button-primary:hover, .dialog-button:hover, .github-row button:hover { background: var(--yellow); box-shadow: 6px 6px 0 var(--black); transform: translate(-2px, -2px) rotate(-1deg); }
.button-primary:active, .dialog-button:active, .github-row button:active { box-shadow: 1px 1px 0 var(--black); transform: translate(3px, 3px); }
/* ---- Inspector ---- */
.inspector { min-width: 0; overflow: hidden; background: var(--ink-2); border-left: 2px solid var(--black); }
.inspector > * { min-width: calc(var(--panel-width) - 2px); }
.tabs { height: 52px; display: flex; align-items: center; gap: 6px; margin: 0 12px; border-bottom: 2px dashed var(--line); }
.tab { height: 30px; padding: 0 11px; border: 2px solid transparent; border-radius: 6px; background: transparent; color: var(--dim); font: 700 11px var(--mono); text-transform: uppercase; letter-spacing: .05em; cursor: pointer; }
.tab:hover { color: var(--paper); }
.tab.active { color: var(--black); border-color: var(--black); background: var(--sky); box-shadow: 2px 2px 0 var(--black); transform: rotate(-2deg); }
.tab:nth-child(2).active { background: var(--tang); transform: rotate(1.5deg); }
.tab:nth-child(3).active { background: var(--pink); transform: rotate(-1deg); }
#resultBadge:not(:empty) { margin-left: 5px; padding: 0 5px; border-radius: 3px; background: var(--black); color: var(--lime); font-size: 10px; }
.panel { display: none; height: calc(100% - 52px); overflow: auto; padding: 16px 14px 26px; }
.panel.active { display: block; } .panel.active { display: block; }
.panel-hint, .result-summary { margin: 0 0 14px; color: var(--muted); line-height: 1.6; } .panel-hint, .result-summary { margin: 0 0 14px; color: var(--muted); line-height: 1.6; }
.entity-title { margin-bottom: 14px; } .entity-title { margin-bottom: 14px; }
.entity-title small { display: block; color: var(--dim); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .entity-title small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dim); font: 11px var(--mono); }
.entity-title h2 { margin: 5px 0 0; color: #f4f6fb; font-size: 19px; font-weight: 650; letter-spacing: -.035em; overflow-wrap: anywhere; } .entity-title h2 { margin: 6px 0 0; font: 400 21px/1.1 var(--display); color: var(--paper); overflow-wrap: anywhere; }
.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 { display: grid; grid-template-columns: 1fr auto; gap: 0 12px; padding: 4px 12px; border: 2px solid var(--black); border-radius: 8px; background: var(--ink); box-shadow: var(--hard); font-variant-numeric: tabular-nums; }
.meta-grid span, .meta-grid b { padding: 7px 0; border-bottom: 1px solid var(--line); } .meta-grid span, .meta-grid b { padding: 7px 0; border-bottom: 1px dashed var(--line); }
.meta-grid span:nth-last-of-type(1), .meta-grid b:nth-last-of-type(1) { border-bottom: 0; } .meta-grid span:nth-last-of-type(1), .meta-grid b:nth-last-of-type(1) { border-bottom: 0; }
.meta-grid span { color: var(--muted); } .meta-grid span { color: var(--muted); }
.meta-grid b { color: var(--text); font-weight: 600; text-align: right; } .meta-grid b { color: var(--paper); font: 700 12px var(--mono); text-align: right; }
.section-title { margin: 20px 0 8px; color: var(--muted); font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; } .section-title { display: flex; align-items: center; gap: 8px; margin: 22px 0 9px; color: var(--paper); font: 700 11px var(--mono); text-transform: uppercase; letter-spacing: .07em; }
.relation { display: flex; gap: 9px; align-items: center; padding: 6px 8px; margin: 0 -8px; border-radius: 8px; cursor: pointer; } .section-title::before { content: ""; width: 9px; height: 9px; flex-shrink: 0; border: 2px solid var(--black); background: var(--pink); transform: rotate(45deg); }
.relation:hover { background: var(--hover); } .relation { display: flex; gap: 9px; align-items: center; margin: 0 -8px; padding: 6px 8px; border-radius: 6px; cursor: pointer; }
.relation i { width: 6px; height: 6px; border-radius: 50%; background: var(--accent-2); box-shadow: 0 0 8px rgba(54,211,194,.6); } .relation:hover { background: var(--ink-3); }
.relation span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #cdd3df; } .relation i { width: 7px; height: 7px; border-radius: 50%; background: var(--lime); }
.relation small { color: var(--dim); font-size: 10px; } .relation span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--paper); }
.alert-row { display: grid; grid-template-columns: auto minmax(0,1fr); gap: 10px; align-items: center; margin: 0 -8px; padding: 7px 8px; border-radius: 9px; color: inherit; text-decoration: none; } .relation small { color: var(--dim); font: 10px var(--mono); }
.alert-row:hover { background: var(--hover); } .alert-row { display: grid; grid-template-columns: auto minmax(0,1fr); gap: 10px; align-items: center; margin: 0 -8px; padding: 7px 8px; border-radius: 6px; color: inherit; text-decoration: none; }
.alert-row em { padding: 2px 7px; border-radius: 999px; font: 700 9.5px var(--mono); font-style: normal; text-transform: uppercase; letter-spacing: .05em; } .alert-row:hover { background: var(--ink-3); }
.alert-row em { padding: 1px 7px; border: 2px solid var(--black); border-radius: 4px; font: 700 9.5px var(--mono); font-style: normal; text-transform: uppercase; }
.alert-row strong, .alert-row small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .alert-row strong, .alert-row small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.alert-row strong { color: var(--text); font-weight: 500; } .alert-row strong { color: var(--paper); }
.alert-row small { color: var(--dim); font-size: 10.5px; } .alert-row small { color: var(--dim); font: 10px var(--mono); }
.severity-critical { color: #fff; background: #d7263d; } .severity-critical { color: var(--paper); background: var(--red); }
.severity-high { color: #1b0d00; background: #ff8a3d; } .severity-high { color: var(--black); background: var(--tang); }
.severity-medium { color: #1b1400; background: #f5c518; } .severity-medium { color: var(--black); background: var(--yellow); }
.severity-low { color: #cfd5e2; background: rgba(255,255,255,.12); } .severity-low { color: var(--black); background: var(--muted); }
.coverage-title, .coverage-list-head, .source-title { display: flex; align-items: baseline; justify-content: space-between; } .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-title small, .coverage-list-head small, .source-title small { color: var(--dim); font: 10px var(--mono); text-transform: none; letter-spacing: 0; }
.coverage-menu { display: grid; gap: 2px; } .coverage-menu { display: grid; gap: 3px; }
.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 { width: 100%; min-height: 34px; display: grid; grid-template-columns: 10px 1fr auto 14px; gap: 10px; align-items: center; padding: 5px 9px; border: 2px solid transparent; border-radius: 7px; background: transparent; color: var(--paper); text-align: left; cursor: pointer; }
.coverage-row:hover { background: var(--hover); } .coverage-row:hover { background: var(--ink-3); }
.coverage-row.active { border-color: var(--accent-line); background: var(--accent-soft); } .coverage-row.active { color: var(--black); border-color: var(--black); background: var(--lime); box-shadow: 2px 2px 0 var(--black); }
.coverage-row i { width: 8px; height: 8px; border-radius: 3px; box-shadow: 0 0 10px currentColor; } .coverage-row i { width: 10px; height: 10px; border: 2px solid var(--black); border-radius: 2px; }
.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: var(--dim); font-variant-numeric: tabular-nums; } .coverage-row small { color: inherit; opacity: .7; font: 11px var(--mono); }
.coverage-row b { color: var(--dim); font-size: 14px; font-weight: 400; text-align: center; } .coverage-row b { font: 700 14px var(--mono); text-align: center; }
.coverage-list-head { margin: 18px 0 8px; color: var(--text); font-weight: 600; } .coverage-list-head { margin: 18px 0 8px; color: var(--paper); font: 700 12px var(--mono); }
.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 { position: relative; height: min(39vh, 350px); min-height: 150px; overflow: auto; border: 2px solid var(--black); border-radius: 8px; outline: none; background: var(--ink); }
.coverage-file-list:focus { border-color: var(--accent-line); box-shadow: 0 0 0 3px rgba(141,125,255,.14); } .coverage-file-list:focus { border-color: var(--pink); }
.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: 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 { width: 100%; height: 43px; display: grid; grid-template-columns: 4px minmax(0,1fr) auto; gap: 10px; align-items: stretch; padding: 5px 10px; border: 0; border-bottom: 1px dashed var(--line); background: transparent; color: inherit; text-align: left; cursor: pointer; }
.coverage-file:hover { background: var(--hover); } .coverage-file:hover { background: var(--ink-3); }
.coverage-file.active { background: var(--accent-soft); box-shadow: inset 2px 0 var(--accent); } .coverage-file.active { background: var(--ink-3); box-shadow: inset 4px 0 var(--pink); }
.coverage-file > i { border-radius: 2px; } .coverage-file > i { border-radius: 1px; }
.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: var(--text); font-weight: 500; } .coverage-file strong { color: var(--paper); }
.coverage-file small { color: var(--dim); font-size: 10px; } .coverage-file small { color: var(--dim); font: 10px var(--mono); }
.coverage-file em { align-self: center; color: var(--dim); font-size: 10px; font-style: normal; font-variant-numeric: tabular-nums; } .coverage-file em { align-self: center; color: var(--dim); font: 10px var(--mono); font-style: normal; }
.coverage-empty { padding: 18px 10px; color: var(--dim); text-align: center; } .coverage-empty { padding: 18px 10px; color: var(--dim); text-align: center; }
.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; } .code-preview { position: relative; height: min(46vh, 430px); min-height: 220px; overflow: auto; border: 2px solid var(--black); border-radius: 8px; outline: none; background: #0e0c09; color: #d9d0bd; font: 11px/16px var(--mono); tab-size: 2; }
.code-preview:focus { border-color: var(--accent-line); box-shadow: 0 0 0 3px rgba(141,125,255,.12); } .code-preview:focus { border-color: var(--pink); }
.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: 6px 0 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: 12px; } .source-line { display: block; height: 16px; padding-right: 12px; }
.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; } .source-line::before { content: attr(data-line); position: sticky; left: 0; display: inline-block; width: 42px; margin-right: 12px; padding-right: 8px; border-right: 2px solid #26221b; background: #0e0c09; color: #5d5647; text-align: right; user-select: none; }
.tok-plain { color: #b6bfd0; } .tok-plain { color: #d9d0bd; }
.tok-comment { color: #5c667a; font-style: italic; } .tok-comment { color: #7d745f; font-style: italic; }
.tok-string { color: #8fd694; } .tok-string { color: #c8f135; }
.tok-number { color: #f5a97f; } .tok-number { color: #ff9a1f; }
.tok-keyword { color: #c69cff; } .tok-keyword { color: #ff5ea8; }
.tok-type { color: #6ec6ff; } .tok-type { color: #5cc8ff; }
.tok-function { color: #7aa7ff; } .tok-function { color: #ffd23f; }
.tok-property { color: #5fd3c6; } .tok-property { color: #9be7c4; }
.tok-operator { color: #8f9ab0; } .tok-operator { color: #a79d88; }
.tok-constant { color: #ff8fa3; } .tok-constant { color: #ff7a6b; }
.tok-tag { color: #5fd3c6; } .tok-tag { color: #5cc8ff; }
.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 { display: grid; grid-template-columns: 4px minmax(0,1fr); gap: 10px; min-width: 0; margin: 0 -6px; padding: 8px; border-radius: 6px; cursor: pointer; }
.result-row:hover { background: var(--hover); } .result-row:hover { background: var(--ink-3); }
.result-row i { border-radius: 2px; background: var(--yellow); } .result-row i { border-radius: 1px; background: var(--tang); }
.result-row strong { display: block; color: var(--text); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .result-row strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--paper); }
.result-row small { display: block; color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .result-row small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dim); font: 10px var(--mono); }
/* Floating canvas chrome */ /* ---- 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); } .breadcrumb { position: absolute; left: 16px; bottom: 16px; max-width: calc(100% - 200px); display: flex; align-items: center; gap: 10px; padding: 6px 13px 6px 6px; border-radius: 10px 4px 10px 4px; color: var(--muted); font: 11px var(--mono); }
#breadcrumbText { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #breadcrumbText { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.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; } .breadcrumb button { width: 28px; height: 28px; display: grid; place-items: center; padding: 0; border: 2px solid var(--black); border-radius: 6px; background: var(--sky); color: var(--black); font-weight: 700; cursor: pointer; transition: transform .15s; }
.breadcrumb button:hover { border-color: var(--accent-line); background: var(--accent-soft); } .breadcrumb button:hover { transform: rotate(-12deg); }
.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); } .fps { position: absolute; right: 20px; bottom: 70px; padding: 2px 7px; border: 2px solid var(--black); border-radius: 4px; background: var(--black); color: var(--lime); font: 700 10px var(--mono); }
.canvas-controls { position: absolute; right: 16px; bottom: 16px; display: flex; gap: 2px; padding: 4px; border-radius: 12px; } .canvas-controls { position: absolute; right: 16px; bottom: 16px; display: flex; gap: 3px; padding: 4px; border-radius: 4px 10px 4px 10px; }
.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; } .canvas-controls button { display: grid; place-items: center; width: 32px; height: 30px; border: 0; border-radius: 6px; background: transparent; color: var(--paper); font: 700 18px var(--mono); cursor: pointer; }
.canvas-controls button:hover { background: var(--hover); color: #fff; } .canvas-controls button:hover { background: var(--lime); color: var(--black); }
.canvas-controls svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; } .canvas-controls svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
.legend { position: absolute; right: 12px; top: 12px; display: none; width: 210px; padding: 6px; border-radius: 12px; } .legend { position: absolute; right: 12px; top: 14px; display: none; width: 214px; padding: 6px; border-radius: 8px; transform: rotate(1deg); }
.legend.open { display: grid; gap: 2px; animation: rise .18s ease-out; } .legend.open { display: grid; gap: 2px; animation: plonk .18s ease-out; }
.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; } .legend button { width: 100%; display: flex; justify-content: space-between; gap: 12px; padding: 7px 8px; border: 0; border-radius: 5px; background: transparent; color: var(--paper); text-align: left; cursor: pointer; }
.legend button:hover { background: var(--hover); } .legend button:hover { background: var(--ink-3); }
.legend button b { color: var(--dim); font-weight: 500; font-variant-numeric: tabular-nums; } .legend button b { color: var(--dim); font: 11px var(--mono); }
.legend i { display: inline-block; width: 8px; height: 8px; margin-right: 8px; border-radius: 3px; } .legend i { display: inline-block; width: 10px; height: 10px; margin-right: 8px; border: 2px solid var(--black); border-radius: 2px; }
.tooltip { position: absolute; z-index: 3; max-width: 320px; padding: 8px 11px; pointer-events: none; border-radius: 10px; color: #fff; font-weight: 500; } .tooltip { position: absolute; z-index: 3; max-width: 320px; padding: 6px 10px; border-radius: 3px; background: var(--paper); color: var(--black); font-weight: 700; pointer-events: none; transform: rotate(-1deg); }
.tooltip small { display: block; margin-top: 1px; color: var(--muted); font-weight: 400; } .tooltip small { display: block; margin-top: 1px; color: #5d5647; font: 400 10.5px var(--mono); }
.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; } .camera-help { position: absolute; z-index: 2; left: 16px; top: 18px; width: fit-content; max-width: calc(100% - 32px); display: flex; flex-wrap: wrap; gap: 12px; align-items: center; padding: 5px 12px 5px 5px; border-radius: 8px; color: var(--dim); font: 10.5px var(--mono); }
.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; } .camera-help button { height: 28px; padding: 0 10px; border: 2px solid var(--black); border-radius: 6px; background: var(--ink-3); color: var(--paper); font: 700 10.5px var(--mono); text-transform: uppercase; cursor: pointer; }
.camera-help button:hover { border-color: var(--accent-line); background: var(--accent-soft); } .camera-help button:hover { background: var(--sky); color: var(--black); }
.camera-help b { color: var(--muted); font-weight: 600; } .camera-help b { color: var(--paper); }
@keyframes rise { from { opacity: 0; transform: translateY(4px); } }
.city-hud { position: absolute; z-index: 2; left: 16px; top: 16px; max-width: calc(100% - 32px); display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding: 5px 12px 5px 5px; border-radius: 12px; color: var(--dim); font-size: 10.5px; } /* City bar: one row of identical chips (views, then actions), help text underneath. */
.city-views { display: flex; gap: 2px; } .city-hud { position: absolute; z-index: 2; left: 16px; top: 18px; max-width: calc(100% - 290px); display: grid; gap: 6px; padding: 5px; border-radius: 10px 4px 10px 4px; }
.city-views button { display: flex; align-items: center; gap: 6px; padding: 5px 10px 5px 6px; border: 1px solid transparent; border-radius: 8px; background: transparent; color: var(--muted); font-size: 11px; font-weight: 500; cursor: pointer; } .city-row { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
.city-views button:hover { color: #fff; background: var(--hover); } .city-row .divider { width: 2px; height: 22px; margin: 0 4px; background: repeating-linear-gradient(var(--line) 0 4px, transparent 4px 7px); }
.city-views button.active { color: #fff; border-color: var(--accent-line); background: var(--accent-soft); } .chip { height: 32px; display: inline-flex; align-items: center; gap: 7px; padding: 0 11px 0 7px; border: 2px solid transparent; border-radius: 7px; background: transparent; color: var(--muted); font: 700 11px var(--mono); text-transform: uppercase; letter-spacing: .03em; white-space: nowrap; cursor: pointer; transition: background-color .12s, color .12s, transform .12s; }
.city-views kbd { padding: 0 4px; font-size: 9px; } .chip svg { width: 16px; height: 16px; flex-shrink: 0; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.city-help b { color: var(--muted); font-weight: 600; } .chip kbd { margin-left: 1px; }
.city-actions { display: flex; gap: 2px; padding-left: 8px; border-left: 1px solid var(--line); } .chip:hover { color: var(--paper); background: var(--ink-3); }
.city-actions button { padding: 5px 10px; border: 1px solid transparent; border-radius: 8px; background: transparent; color: var(--muted); font-size: 11px; font-weight: 500; cursor: pointer; white-space: nowrap; } .chip:hover svg { animation: wiggle .35s ease-in-out; }
.city-actions button:hover { color: #fff; background: var(--hover); } .chip.active { color: var(--black); border-color: var(--black); background: var(--lime); box-shadow: 2px 2px 0 var(--black); }
.city-actions button.active { color: #fff; border-color: var(--accent-line); background: var(--accent-soft); } #tourButton.active { background: var(--pink); }
.tour-caption { position: absolute; z-index: 2; left: 50%; bottom: 84px; transform: translateX(-50%); display: grid; justify-items: center; gap: 2px; padding: 12px 22px; border-radius: 16px; text-align: center; pointer-events: none; animation: rise .3s ease-out; } #shareButton.done { color: var(--black); border-color: var(--black); background: var(--sky); }
.tour-caption small { color: var(--accent-2); font: 600 10px var(--mono); letter-spacing: .12em; } .city-help { padding: 0 6px 2px; color: var(--dim); font: 10.5px var(--mono); }
.tour-caption strong { color: #fff; font-size: 22px; font-weight: 700; letter-spacing: -.03em; } .city-help b { color: var(--paper); }
.tour-caption span { color: var(--muted); font-size: 11.5px; } .tour-caption { position: absolute; z-index: 2; left: 50%; bottom: 84px; display: grid; justify-items: center; gap: 2px; padding: 12px 22px; border-radius: 10px; text-align: center; pointer-events: none; transform: translateX(-50%) rotate(-1.5deg); animation: plonk .3s ease-out; }
.joystick { position: absolute; z-index: 3; width: 112px; height: 112px; margin: -56px 0 0 -56px; border: 1px solid var(--line-strong); border-radius: 50%; background: rgba(16,19,27,.45); pointer-events: none; } .tour-caption small { padding: 0 6px; background: var(--pink); color: var(--black); font: 700 10px var(--mono); letter-spacing: .1em; }
.joystick i { position: absolute; left: 36px; top: 36px; width: 40px; height: 40px; border-radius: 50%; background: var(--accent-grad); box-shadow: 0 0 16px rgba(95,157,255,.6); } .tour-caption strong { color: var(--paper); font: 400 26px var(--display); }
.tour-caption span { color: var(--muted); font: 11px var(--mono); }
.joystick { position: absolute; z-index: 3; width: 112px; height: 112px; margin: -56px 0 0 -56px; border: 2px dashed var(--paper); border-radius: 50%; background: rgba(21,19,15,.5); pointer-events: none; }
.joystick i { position: absolute; left: 34px; top: 34px; width: 40px; height: 40px; border: 2px solid var(--black); border-radius: 50%; background: var(--lime); }
.crosshair { position: absolute; z-index: 2; left: 50%; top: 50%; width: 18px; height: 18px; margin: -9px 0 0 -9px; pointer-events: none; } .crosshair { position: absolute; z-index: 2; left: 50%; top: 50%; width: 18px; height: 18px; margin: -9px 0 0 -9px; pointer-events: none; }
.crosshair::before, .crosshair::after { content: ""; position: absolute; background: rgba(255,255,255,.85); box-shadow: 0 0 6px rgba(0,0,0,.8); } .crosshair::before, .crosshair::after { content: ""; position: absolute; background: var(--paper); box-shadow: 0 0 0 1px var(--black); }
.crosshair::before { left: 8px; top: 0; width: 2px; height: 18px; } .crosshair::before { left: 8px; top: 0; width: 2px; height: 18px; }
.crosshair::after { left: 0; top: 8px; width: 18px; height: 2px; } .crosshair::after { left: 0; top: 8px; width: 18px; height: 2px; }
.city-target { position: absolute; z-index: 2; left: 50%; top: calc(50% + 22px); transform: translateX(-50%); padding: 6px 11px; border-radius: 10px; color: #fff; font-weight: 500; white-space: nowrap; pointer-events: none; text-align: center; } .city-target { position: absolute; z-index: 2; left: 50%; top: calc(50% + 22px); padding: 5px 11px; border-radius: 4px; background: var(--paper); color: var(--black); font-weight: 700; white-space: nowrap; text-align: center; pointer-events: none; transform: translateX(-50%) rotate(-1deg); }
.city-target small { display: block; color: var(--muted); font-weight: 400; font-size: 10.5px; } .city-target small { display: block; color: #5d5647; font: 400 10.5px var(--mono); }
.city-target kbd { margin-left: 6px; } .city-target kbd { margin-left: 6px; background: var(--lime); }
.city-address { position: absolute; z-index: 2; left: 16px; top: 64px; max-width: calc(100% - 32px); padding: 6px 12px; border-radius: 999px; color: var(--text); font: 500 11px var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; pointer-events: none; } .city-address { position: absolute; z-index: 2; right: 16px; top: 18px; max-width: 260px; padding: 4px 10px; border-radius: 4px; background: var(--black); color: var(--lime); font: 700 11px var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; pointer-events: none; }
.minimap { position: absolute; z-index: 2; right: 16px; bottom: 62px; width: 220px; height: 150px; border-radius: 12px; cursor: crosshair; } .minimap { position: absolute; z-index: 2; right: 16px; bottom: 62px; width: 220px; height: 150px; border-radius: 6px; cursor: crosshair; transform: rotate(-1deg); }
.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; }
@keyframes drop { from { opacity: 0; transform: translate(-50%, -8px); } } .progress-card { position: absolute; z-index: 4; left: 50%; top: 22px; width: min(460px, calc(100% - 30px)); padding: 16px 44px 15px 16px; border-radius: 10px 4px 10px 4px; box-shadow: var(--hard-lg); transform: translateX(-50%) rotate(-.6deg); animation: plonk .22s ease-out; }
.progress-copy { display: flex; flex-direction: column; gap: 3px; } .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 strong { color: var(--paper); font: 400 15px var(--display); }
.progress-copy span, .progress-count { color: var(--muted); } .progress-copy span, .progress-count { color: var(--muted); }
#progressMessage { max-height: 100px; overflow: auto; overflow-wrap: anywhere; } #progressMessage { max-height: 100px; overflow: auto; overflow-wrap: anywhere; font: 11px var(--mono); }
.progress-track { position: relative; height: 4px; margin: 13px 0 7px; overflow: hidden; border-radius: 999px; background: rgba(255,255,255,.07); } .progress-track { position: relative; height: 12px; margin: 12px 0 7px; overflow: hidden; border: 2px solid var(--black); border-radius: 3px; background: var(--ink); }
.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 { display: block; height: 100%; width: 5%; background: repeating-linear-gradient(-45deg, var(--lime) 0 7px, var(--black) 7px 14px) 0 0 / 28px 100%; animation: barber .6s linear infinite; 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; } .progress-count { font: 10.5px var(--mono); }
@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-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; } .progress-action { display: inline-grid; place-items: center; width: fit-content; 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 { position: absolute; inset: 0; display: grid; place-content: center; gap: 4px; text-align: center; color: var(--paper); }
.empty-state strong { font: 400 20px var(--display); }
.empty-state span { color: var(--muted); } .empty-state span { color: var(--muted); }
/* Dialogs */ /* ---- Dialogs ---- */
dialog { dialog { width: min(580px, calc(100vw - 30px)); max-height: calc(100dvh - 32px); padding: 0; border: 2px solid var(--black); border-radius: 16px 6px 16px 6px; background: var(--ink-2); color: var(--paper); box-shadow: 8px 8px 0 var(--black); }
width: min(580px, calc(100vw - 30px)); dialog[open] { animation: plonk .2s ease-out; transform: rotate(-.4deg); }
max-height: calc(100dvh - 32px); dialog::backdrop { background: rgba(10, 8, 5, .78); }
padding: 0; .dialog-shell { padding: 28px; }
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-head { display: flex; align-items: start; justify-content: space-between; margin-bottom: 18px; } .dialog-head { display: flex; align-items: start; justify-content: space-between; margin-bottom: 18px; }
.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 small { display: inline-block; padding: 2px 7px; background: var(--pink); color: var(--black); font: 700 10px var(--mono); letter-spacing: .12em; transform: rotate(-2deg); }
.dialog-head h1 { margin: 8px 0 0; font-size: 28px; font-weight: 700; letter-spacing: -.045em; color: #f4f6fb; } .dialog-head h1 { margin: 10px 0 0; font: 400 30px/1.05 var(--display); color: var(--paper); }
.dialog-description { margin: 8px 0 2px; color: var(--muted); font-size: 13px; } .dialog-description { margin: 8px 0 2px; color: var(--muted); font-size: 14px; }
.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 { width: 34px; height: 34px; flex-shrink: 0; display: grid; place-items: center; padding: 0; border: 2px solid var(--black); border-radius: 8px; background: var(--ink-3); color: var(--paper); cursor: pointer; transition: transform .2s, background-color .12s; }
.close-button svg { display: block; width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; } .close-button svg { display: block; width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; }
.close-button:hover { border-color: var(--line); background: var(--hover); color: #fff; } .close-button:hover { background: var(--red); color: var(--black); transform: rotate(90deg); }
.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; } .source-card { width: 100%; margin-top: 14px; padding: 16px; border: 2px solid var(--black); border-radius: 10px; background: var(--ink); color: inherit; text-align: left; transition: transform .12s, box-shadow .12s; }
button.source-card { display: flex; align-items: center; gap: 14px; cursor: pointer; } button.source-card { display: flex; align-items: center; gap: 14px; cursor: pointer; }
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); } button.source-card:hover { box-shadow: 5px 5px 0 var(--sky); transform: translate(-2px, -2px); }
.source-card:focus-within:not(button) { border-color: rgba(255,255,255,.14); } .github-card:focus-within { box-shadow: 5px 5px 0 var(--lime); }
.source-card > b { margin-left: auto; color: var(--dim); font-size: 18px; transition: transform .18s, color .18s; } .source-card > b { margin-left: auto; color: var(--dim); font: 700 20px var(--mono); transition: transform .12s, color .12s; }
button.source-card:hover > b { transform: translateX(3px); color: #fff; } button.source-card:hover > b { color: var(--sky); transform: translateX(4px); }
.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 { width: 42px; height: 42px; display: grid; place-items: center; border: 2px solid var(--black); border-radius: 8px; background: var(--sky); color: var(--black); transform: rotate(-5deg); }
.source-icon svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; } .github-card .source-icon { background: var(--lime); transform: rotate(4deg); }
.source-icon svg { width: 21px; height: 21px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.source-card strong, .source-card small { display: block; } .source-card strong, .source-card small { display: block; }
.source-card strong { color: #f1f3f8; font-size: 13.5px; font-weight: 600; } .source-card strong { font: 400 15px var(--display); color: var(--paper); }
.source-card small { margin-top: 2px; color: var(--muted); font-weight: 400; } .source-card small { margin-top: 2px; color: var(--muted); }
.github-card { display: grid; grid-template-columns: 40px 1fr; gap: 0 14px; align-items: center; } .github-card { display: grid; grid-template-columns: 42px 1fr; gap: 0 14px; align-items: center; }
.github-row { grid-column: 1 / -1; display: flex; gap: 8px; margin-top: 14px; } .github-row { grid-column: 1 / -1; display: flex; gap: 10px; margin-top: 14px; }
.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 input { flex: 1; min-width: 0; height: 42px; padding: 0 12px; border: 2px solid var(--black); border-radius: 8px; outline: none; background: var(--ink-2); color: var(--paper); font: 12px var(--mono); }
.github-row input::placeholder { color: var(--dim); } .github-row input::placeholder { color: var(--dim); }
.github-row input:focus { border-color: var(--accent-line); box-shadow: 0 0 0 4px rgba(141,125,255,.14); } .github-row input:focus { border-color: var(--lime); }
.github-row button { height: 42px; } .github-account { grid-column: 1 / -1; margin: 10px 0 0; color: var(--muted); font-size: 12.5px; }
.github-account { grid-column: 1 / -1; margin: 10px 0 0; color: var(--muted); font-size: 11.5px; } .github-account a, .github-account button { padding: 0 2px; border: 0; background: linear-gradient(transparent 60%, rgba(255,94,168,.45) 60%); color: var(--paper); font: inherit; font-weight: 700; text-decoration: none; cursor: pointer; }
.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; } .github-account a:hover, .github-account button:hover { background: var(--pink); color: var(--black); }
.github-account a:hover, .github-account button:hover { color: #fff; border-bottom-color: #fff; } .github-notice { margin: 0 0 4px; padding: 9px 12px; border: 2px solid var(--black); border-left-width: 8px; border-radius: 6px; background: var(--ink); color: var(--paper); font-weight: 700; transform: rotate(-.5deg); }
.github-notice { margin: 0 0 4px; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line); background: var(--surface-2); color: var(--text); font-weight: 500; } .github-notice[data-tone="ok"] { border-left-color: var(--lime); }
.github-notice[data-tone="ok"] { border-color: rgba(54,211,160,.5); background: rgba(54,211,160,.1); } .github-notice[data-tone="warn"] { border-left-color: var(--yellow); }
.github-notice[data-tone="warn"] { border-color: rgba(242,201,76,.5); background: rgba(242,201,76,.1); } .github-notice[data-tone="error"] { border-left-color: var(--red); }
.github-notice[data-tone="error"] { border-color: rgba(255,107,122,.5); background: rgba(255,107,122,.1); } .form-error { grid-column: 1 / -1; min-height: 16px; margin: 6px 0 -6px; color: var(--red); font: 700 11px var(--mono); }
.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: 11px/1.6 var(--mono); }
.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: 16px; height: 16px; display: grid; place-items: center; border-radius: 50%; background: var(--yellow); color: var(--black); font-weight: 700; }
.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-dialog { width: min(480px, calc(100vw - 30px)); }
.settings-shell { padding: 28px; } .settings-dialog[open] { transform: rotate(.5deg); }
.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-shell { padding: 26px; }
.settings-status span { display: flex; align-items: center; gap: 8px; color: var(--text); font-weight: 600; } .settings-status { display: flex; align-items: center; justify-content: space-between; margin: -2px 0 8px; padding: 8px 12px; border: 2px dashed var(--line); border-radius: 8px; }
.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 span { display: flex; align-items: center; gap: 8px; color: var(--paper); font-weight: 700; }
.settings-status small { color: var(--dim); font-size: 10px; letter-spacing: .08em; } .settings-status i { width: 9px; height: 9px; border: 2px solid var(--black); border-radius: 50%; background: var(--lime); }
.preference-row { display: flex; gap: 16px; justify-content: space-between; align-items: center; padding: 16px 0; border-bottom: 1px solid var(--line); } .settings-status small { color: var(--dim); font: 700 10px var(--mono); letter-spacing: .08em; }
.preference-row label { color: var(--text); font-weight: 500; } .preference-row { display: flex; gap: 16px; justify-content: space-between; align-items: center; padding: 15px 0; border-bottom: 1px dashed var(--line); }
.preference-row small { display: block; margin-top: 3px; color: var(--muted); font-size: 11px; font-weight: 400; } .preference-row label { color: var(--paper); font-weight: 700; }
.preference-row select { padding: 8px 10px; border: 1px solid var(--line-strong); border-radius: 10px; background: rgba(0,0,0,.35); color: var(--text); } .preference-row small { display: block; margin-top: 3px; color: var(--muted); font-size: 11.5px; font-weight: 400; }
.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; } .preference-row select { padding: 7px 10px; border: 2px solid var(--black); border-radius: 7px; background: var(--ink); color: var(--paper); font: 12px var(--mono); }
.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 { appearance: none; flex-shrink: 0; width: 44px; height: 24px; margin: 0; padding: 2px; border: 2px solid var(--black); border-radius: 999px; background: var(--ink-3); cursor: pointer; transition: background .15s; }
.switch:checked { border-color: transparent; background: var(--accent-grad); } .switch::before { content: ""; display: block; width: 16px; height: 16px; border: 2px solid var(--black); border-radius: 50%; background: var(--muted); transition: transform .2s cubic-bezier(.3,1.6,.5,1), background .15s; }
.switch:checked::before { background: #fff; transform: translateX(16px); } .switch:checked { background: var(--lime); }
.shortcut-guide { margin-top: 18px; color: var(--muted); font-size: 11px; } .switch:checked::before { background: var(--paper); transform: translateX(20px); }
.shortcut-guide { margin-top: 18px; color: var(--muted); font: 11px var(--mono); }
.shortcut-guide > span { display: block; margin-bottom: 10px; } .shortcut-guide > span { display: block; margin-bottom: 10px; }
.shortcut-guide div { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; } .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 { display: grid; grid-template-columns: 36px 1fr; gap: 12px; align-items: start; margin-top: 16px; padding: 12px; border: 2px solid var(--black); border-radius: 10px; background: var(--ink); transform: rotate(-.7deg); }
.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-icon { width: 36px; height: 36px; display: grid; place-items: center; border: 2px solid var(--black); border-radius: 8px; background: var(--tang); color: var(--black); font-size: 18px; transform: rotate(8deg); }
.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: var(--text); font-size: 12px; } .settings-note strong { color: var(--paper); }
.settings-note small { margin-top: 3px; color: var(--muted); line-height: 1.5; } .settings-note small { margin-top: 3px; color: var(--muted); line-height: 1.5; }
.settings-actions { display: flex; justify-content: flex-end; margin-top: 18px; } .settings-actions { display: flex; justify-content: flex-end; margin-top: 20px; }
.dialog-button { min-width: 90px; } .dialog-button { min-width: 96px; }
button:focus-visible, a:focus-visible, input:focus-visible, select:focus-visible, [role="tab"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; } button:focus-visible, a:focus-visible, input:focus-visible, select:focus-visible, [role="tab"]:focus-visible { outline: 2px dashed var(--pink); outline-offset: 3px; }
html[data-input-method="pointer"] button:focus { outline: none; } html[data-input-method="pointer"] button:focus { outline: none; }
@media (max-width: 1250px) { .scene-stats { display: none; } } @media (max-width: 1500px) { .scene-stats { display: none; } }
@media (max-width: 1150px) {
.tool .label, .metric span, .brand-name { display: none; }
.tool:not(.icon-only) { padding: 0 8px; }
.brand { padding-right: 10px; }
.welcome-sticker { display: none; }
}
@media (max-width: 900px) { @media (max-width: 900px) {
:root { --panel-width: 260px; } :root { --panel-width: 260px; }
.metric span, .brand-name { display: none; }
.brand { padding-right: 10px; }
.filter-wrap { width: 160px; } .filter-wrap { width: 160px; }
.toolbar { gap: 4px; padding: 0 10px; } .toolbar { gap: 6px; padding: 0 10px; }
.chip .label { display: none; }
.city-hud { max-width: calc(100% - 32px); }
.city-address { display: none; }
} }
@media (max-width: 670px) { @media (max-width: 670px) {
:root { --panel-width: 0px; } :root { --panel-width: 0px; }
.workspace { grid-template-columns: 1fr; grid-template-rows: minmax(160px,1fr) minmax(150px,30%); } .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: 2px solid var(--black); }
.tabs { height: 40px; } .inspector > * { min-width: 0; }
.tab { height: 40px; } .inspector-toggle { display: none; }
.panel { height: calc(100% - 40px); padding: 10px 12px; } .tabs { height: 44px; }
.separator, #layersButton, #codeButton, .palette, .brand, .metric-switch, #mapButton, .toolbar-spacer { display: none; } .panel { height: calc(100% - 44px); padding: 10px 12px; }
#layersButton, #codeButton, #paletteButton, .brand, .metric-switch, #mapButton, .toolbar-spacer { display: none; }
.filter-wrap { flex: 1; width: auto; min-width: 65px; } .filter-wrap { flex: 1; width: auto; min-width: 65px; }
.dialog-shell { padding: 22px; } .dialog-shell { padding: 20px; }
.welcome { padding: 20px; } .welcome { padding: 20px; }
.welcome-features { display: none; } .welcome-features { display: none; }
.camera-help { left: 10px; top: 10px; gap: 8px; font-size: 9.5px; } .camera-help { left: 10px; top: 10px; gap: 8px; font-size: 9.5px; }
.city-hud { left: 10px; top: 10px; gap: 6px; } .city-hud { left: 10px; top: 10px; }
.city-help, .minimap { display: none; } .city-help, .minimap { display: none; }
.city-address { left: 10px; top: 58px; }
.breadcrumb { left: 10px; bottom: 10px; } .breadcrumb { left: 10px; bottom: 10px; }
.canvas-controls { right: 10px; bottom: 10px; } .canvas-controls { right: 10px; bottom: 10px; }
} }
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; } } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; } }