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:
+32
-14
@@ -118,7 +118,7 @@ function computeLayout(){
|
||||
function computeCity(){
|
||||
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}];
|
||||
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=[];
|
||||
for(const item of layoutItems){
|
||||
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);
|
||||
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))}));
|
||||
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();
|
||||
renderer.setCity(instances,{width:cityModel.width,height:cityModel.height});
|
||||
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 ----
|
||||
let currentRepoUrl='';
|
||||
$('#shareButton').addEventListener('click',async event=>{
|
||||
const button=event.currentTarget,label=button.textContent;button.blur();
|
||||
const flash=text=>{button.textContent=text;setTimeout(()=>{button.textContent=label;},1800);};
|
||||
const button=event.currentTarget,labelElement=button.querySelector('.label'),label=labelElement.textContent;button.blur();
|
||||
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;}
|
||||
const hash=encodePlace({repo:currentRepoUrl,view:renderer.mode,camera:renderer.city,file:selected?.path});
|
||||
window.history.replaceState(null,'',hash);
|
||||
@@ -372,8 +372,8 @@ function minimapTransform(){
|
||||
function buildMinimap(){
|
||||
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);
|
||||
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.fillStyle='rgba(141,125,255,.08)';
|
||||
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(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 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));}
|
||||
@@ -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'};
|
||||
// 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 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;
|
||||
@@ -605,9 +605,9 @@ function selectFile(file,addHistory=true){
|
||||
|
||||
function renderInspector(){
|
||||
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){
|
||||
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 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>`:''}`;
|
||||
@@ -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 drawChip(ctx,text,x,y,maxWidth,accent,occupied,force=false,align='left'){
|
||||
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;
|
||||
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);
|
||||
@@ -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};
|
||||
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);
|
||||
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);}
|
||||
ctx.fillStyle='rgba(237,241,238,.94)';ctx.fillText(label,x+5,y+2);
|
||||
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');
|
||||
}
|
||||
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;
|
||||
codeTextureDeadline=performance.now()+5;
|
||||
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());
|
||||
$('#localFolderButton').addEventListener('click',async()=>{
|
||||
if(!localFoldersEnabled)return;
|
||||
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);}}
|
||||
else $('#folderFallback').click();
|
||||
@@ -1323,12 +1324,17 @@ async function resumeAfterGithub(){
|
||||
requestAnimationFrame(()=>(pending&&tone==='ok'?$('#githubButton'):$('#githubInput')).focus());
|
||||
}
|
||||
|
||||
let localFoldersEnabled=false;
|
||||
$$('.local-only').forEach(element=>{element.hidden=true;});
|
||||
async function checkBackend(){
|
||||
const indicator=$('.live-dot');
|
||||
try{
|
||||
const response=await apiFetch(apiUrl('/api/health'));
|
||||
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');
|
||||
// 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{
|
||||
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();});});
|
||||
switchTab('inspector');
|
||||
new ResizeObserver(()=>{dirty=true;}).observe(viewport);
|
||||
$('#settingsButton').title='Workspace settings';$('#settingsButton').setAttribute('aria-label','Workspace settings');
|
||||
$('#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"/>';
|
||||
// Sidebar folds away with the tab on its left edge (or [ ); remembered on this device.
|
||||
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());
|
||||
|
||||
+6
-4
@@ -1,6 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<rect width="64" height="64" rx="14" fill="#171918"/>
|
||||
<path d="M10 13h44v38H10z" fill="#202b28" stroke="#65b3ff" stroke-width="3"/>
|
||||
<path d="M10 29h44M29 13v38M29 40h25" fill="none" stroke="#29c997" stroke-width="3"/>
|
||||
<path d="M15 19h9M15 23h7M34 19h14M34 23h10M15 35h9M15 39h7M34 46h13" fill="none" stroke="#d6dad7" stroke-linecap="round" stroke-width="2" opacity=".82"/>
|
||||
<rect width="64" height="64" rx="12" fill="#1e1b16"/>
|
||||
<g stroke="#000" stroke-width="3" transform="rotate(-6 32 32)">
|
||||
<rect x="8" y="24" width="15" height="32" rx="2" fill="#c8f135"/>
|
||||
<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>
|
||||
|
||||
|
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
@@ -3,46 +3,45 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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.">
|
||||
<title>CodeNavigator</title>
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
<main id="app">
|
||||
<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="tool-group">
|
||||
<button class="tool primary" id="loadButton" title="Open codebase" aria-label="Open codebase"><svg viewBox="0 0 24 24"><path d="M3.5 7.5h6l2-2h4l2 2h3v11h-17z"/><path d="M3.5 9.5h17"/></svg></button>
|
||||
<button class="tool 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" 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>
|
||||
<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>
|
||||
<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>
|
||||
<div class="tool-group" role="group" aria-label="View mode">
|
||||
<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" 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>
|
||||
<span class="separator"></span>
|
||||
<div class="tool-group view-switch" role="group" aria-label="View mode">
|
||||
<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" 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" 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" 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 class="tool-group" role="group" aria-label="Map options">
|
||||
<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" 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" 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 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>
|
||||
</div>
|
||||
<span class="separator"></span>
|
||||
<div class="metric-switch" role="group" aria-label="Map metric">
|
||||
<button class="metric active" data-metric="lines"><b>#</b> <span>Lines</span></button>
|
||||
<button class="metric" data-metric="references"><b>⌁</b> <span>References</span></button>
|
||||
<div class="tool-group 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" data-metric="references" title="Size tiles by references"><b>⌁</b> <span>Refs</span></button>
|
||||
</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="scene-stats" id="sceneStats" aria-live="polite">
|
||||
<span id="headlineStats">No codebase loaded</span>
|
||||
<small id="fileStats">Open a folder or public repository</small>
|
||||
<span id="headlineStats">no repo yet</span>
|
||||
<small id="fileStats">nothing open yet</small>
|
||||
</div>
|
||||
<label class="filter-wrap">
|
||||
<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>
|
||||
</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>
|
||||
</header>
|
||||
|
||||
@@ -51,15 +50,16 @@
|
||||
<canvas id="glCanvas"></canvas>
|
||||
<canvas id="overlayCanvas"></canvas>
|
||||
<div class="welcome" id="welcome">
|
||||
<span class="eyebrow"><i></i>Codebase landscapes</span>
|
||||
<h1>See your code<br><span>as a place.</span></h1>
|
||||
<p>Every file becomes a tile sized by its lines, grouped by folder and coloured by purpose. Fly over it in 2D or 3D, search every symbol, and follow imports from file to file.</p>
|
||||
<div class="welcome-actions"><button type="button" class="button-primary" id="welcomeOpen">Open a codebase</button><span><kbd>Ctrl</kbd> <kbd>O</kbd></span></div>
|
||||
<div class="welcome-features"><span>Public & private GitHub repos</span><span>Local folders</span><span>Symbol search</span><span>Import graph</span></div>
|
||||
<span class="eyebrow"><i></i>code, but it's a place now</span>
|
||||
<h1>See your code<br><span>as a city.</span></h1>
|
||||
<div class="welcome-sticker" aria-hidden="true">now with fire<small>(security alerts)</small></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-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 class="empty-state" id="emptyState" hidden>
|
||||
<strong>No source files found</strong>
|
||||
<span>Try another folder or public GitHub repository.</span>
|
||||
<strong>Nothing to see here</strong>
|
||||
<span>No source files in there. Try another repo?</span>
|
||||
</div>
|
||||
<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>
|
||||
@@ -82,14 +82,13 @@
|
||||
<span><b>F</b> reset</span>
|
||||
</div>
|
||||
<div class="city-hud" id="cityHud" hidden>
|
||||
<div class="city-views" role="group" aria-label="City camera">
|
||||
<button type="button" data-city-view="heli" title="Helicopter (1)"><kbd>1</kbd> Helicopter</button>
|
||||
<button type="button" data-city-view="walk" title="Walk (2)"><kbd>2</kbd> Walk</button>
|
||||
<button type="button" data-city-view="fly" title="Fly (3)"><kbd>3</kbd> Fly</button>
|
||||
</div>
|
||||
<div class="city-actions">
|
||||
<button type="button" id="tourButton" title="Guided tour (T)">Tour</button>
|
||||
<button type="button" id="shareButton" title="Copy a link to this place">Copy link</button>
|
||||
<div class="city-row" role="group" aria-label="City camera">
|
||||
<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" 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" 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>
|
||||
<span class="divider" aria-hidden="true"></span>
|
||||
<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" 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>
|
||||
</div>
|
||||
<span class="city-help" id="cityHelp"></span>
|
||||
</div>
|
||||
@@ -101,13 +100,14 @@
|
||||
<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">
|
||||
<button id="homeButton" aria-label="Fit entire codebase">←</button>
|
||||
<span id="breadcrumbText">No codebase loaded</span>
|
||||
<span id="breadcrumbText">nothing open</span>
|
||||
</nav>
|
||||
<div class="fps" id="fps">— fps</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>
|
||||
|
||||
<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">
|
||||
<div class="tabs" role="tablist">
|
||||
<button class="tab active" data-tab="inspector" role="tab">Inspector</button>
|
||||
@@ -133,11 +133,11 @@
|
||||
<dialog id="loadDialog" aria-labelledby="loadTitle">
|
||||
<form method="dialog" class="dialog-shell">
|
||||
<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>
|
||||
</div>
|
||||
<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><strong>Local folder</strong><small>Temporarily indexed by this server</small></span>
|
||||
<b>→</b>
|
||||
@@ -156,7 +156,7 @@
|
||||
<dialog id="settingsDialog" class="settings-dialog" aria-labelledby="settingsTitle">
|
||||
<form method="dialog" class="dialog-shell settings-shell">
|
||||
<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>
|
||||
</div>
|
||||
<div id="settingsContent"></div>
|
||||
|
||||
+8
-8
@@ -17,15 +17,15 @@ export class ShelfPacker {
|
||||
|
||||
const STYLES = {
|
||||
// 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)' },
|
||||
selected: { font: '700 30px Inter, system-ui, sans-serif', text: '#1a1405', fill: 'rgba(255,236,140,.96)', border: 'rgba(255,255,255,.5)' },
|
||||
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 "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.
|
||||
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.
|
||||
tapeDependabot: { font: '800 28px Inter, system-ui, 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 },
|
||||
tapeDependabot: { font: '400 28px "Archivo Black", Impact, sans-serif', text: '#141414', fill: '#f5c518', stripe: '#141414', 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.
|
||||
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 {
|
||||
@@ -166,9 +166,9 @@ function paintPoster(ctx, { title, body }, w, h, theme, variant) {
|
||||
const shape = variant % 4;
|
||||
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 === 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); }
|
||||
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 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);
|
||||
|
||||
+5
-5
@@ -107,7 +107,7 @@ void main(){
|
||||
outColor = vec4(vColor.rgb * pow(1.0 - vUnit.z, 1.6) * .55, 0.0);
|
||||
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){
|
||||
// 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;
|
||||
@@ -117,7 +117,7 @@ void main(){
|
||||
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 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;
|
||||
}
|
||||
#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 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.
|
||||
vec3 top = vec3(.055, .067, .094);
|
||||
vec3 top = vec3(.086, .078, .059);
|
||||
vec3 base = uMode < .5 ? mix(top, vColor.rgb, .07) : top;
|
||||
#ifdef CITY
|
||||
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
|
||||
precision highp float;
|
||||
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.
|
||||
const SIGN_VS = `#version 300 es
|
||||
@@ -726,7 +726,7 @@ export class LandscapeRenderer {
|
||||
|
||||
render(alpha = 1) {
|
||||
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);
|
||||
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);
|
||||
|
||||
+305
-362
@@ -1,438 +1,381 @@
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 400 700;
|
||||
font-display: swap;
|
||||
src: url("./inter-latin.woff2") format("woff2");
|
||||
}
|
||||
/* CodeNavigator: a hand-cut dev zine. Flat ink, hard edges, stickers, a little chaos. */
|
||||
@font-face { font-family: "Archivo Black"; font-weight: 400; font-display: swap; src: url("./fonts/archivo-black.woff2") format("woff2"); }
|
||||
@font-face { font-family: "Atkinson Hyperlegible"; font-weight: 400; font-display: swap; src: url("./fonts/atkinson-400.woff2") format("woff2"); }
|
||||
@font-face { font-family: "Atkinson Hyperlegible"; font-weight: 700; font-display: swap; src: url("./fonts/atkinson-700.woff2") format("woff2"); }
|
||||
@font-face { font-family: "Space Mono"; font-weight: 400; font-display: swap; src: url("./fonts/space-mono-400.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 {
|
||||
color-scheme: dark;
|
||||
--bg: #07090d;
|
||||
--bg-raised: #0d1017;
|
||||
--surface: rgba(16, 19, 27, .74);
|
||||
--surface-solid: #10131b;
|
||||
--surface-2: rgba(255, 255, 255, .035);
|
||||
--hover: rgba(255, 255, 255, .06);
|
||||
--line: rgba(255, 255, 255, .08);
|
||||
--line-strong: rgba(255, 255, 255, .15);
|
||||
--text: #e9ecf3;
|
||||
--muted: #9ba3b4;
|
||||
--dim: #687083;
|
||||
--accent: #8d7dff;
|
||||
--accent-2: #36d3c2;
|
||||
--accent-grad: linear-gradient(135deg, #9a7dff 0%, #5f9dff 50%, #36d3c2 100%);
|
||||
--accent-soft: rgba(141, 125, 255, .16);
|
||||
--accent-line: rgba(141, 125, 255, .5);
|
||||
--green: #36d3a0;
|
||||
--yellow: #f2c94c;
|
||||
--danger: #ff6b7a;
|
||||
--glass: blur(18px) saturate(150%);
|
||||
--shadow: 0 18px 50px rgba(0, 0, 0, .45), 0 2px 8px rgba(0, 0, 0, .3);
|
||||
--panel-width: 300px;
|
||||
--toolbar-height: 52px;
|
||||
--mono: "JetBrains Mono", "SFMono-Regular", ui-monospace, Consolas, monospace;
|
||||
--ink: #15130f;
|
||||
--ink-2: #1e1b16;
|
||||
--ink-3: #2a261f;
|
||||
--black: #000;
|
||||
--paper: #f3ecdc;
|
||||
--muted: #bdb39d;
|
||||
--dim: #867c67;
|
||||
--line: #3b352b;
|
||||
--lime: #c8f135;
|
||||
--pink: #ff5ea8;
|
||||
--sky: #5cc8ff;
|
||||
--tang: #ff9a1f;
|
||||
--red: #ff4d3d;
|
||||
--yellow: #ffd23f;
|
||||
--danger: #ff4d3d;
|
||||
--hard: 3px 3px 0 var(--black);
|
||||
--hard-lg: 5px 5px 0 var(--black);
|
||||
--display: "Archivo Black", Impact, "Arial Black", sans-serif;
|
||||
--body: "Atkinson Hyperlegible", system-ui, sans-serif;
|
||||
--mono: "Space Mono", ui-monospace, Consolas, monospace;
|
||||
--panel-width: 310px;
|
||||
--toolbar-height: 56px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
[hidden] { display: none !important; }
|
||||
html, body, #app { width: 100%; height: 100%; overflow: hidden; }
|
||||
body {
|
||||
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;
|
||||
}
|
||||
body { margin: 0; background: var(--ink); color: var(--paper); font: 13px/1.5 var(--body); -webkit-font-smoothing: antialiased; }
|
||||
button, input, select { font: 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; }
|
||||
::selection { background: rgba(141, 125, 255, .35); color: #fff; }
|
||||
* { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.14) transparent; }
|
||||
kbd { font: 500 10px/1.5 var(--mono); border: 1px solid var(--line-strong); border-bottom-width: 2px; border-radius: 5px; padding: 0 5px; color: #cfd5e2; background: rgba(255,255,255,.04); }
|
||||
::selection { background: var(--pink); color: var(--black); }
|
||||
* { scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
|
||||
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 */
|
||||
.toolbar {
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
height: var(--toolbar-height);
|
||||
padding: 0 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: linear-gradient(180deg, rgba(18, 21, 30, .96), rgba(11, 13, 19, .96));
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.toolbar::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0; right: 0; bottom: -1px;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(141,125,255,.55) 30%, rgba(54,211,194,.45) 70%, transparent);
|
||||
opacity: .55;
|
||||
}
|
||||
.brand { display: flex; gap: 10px; align-items: center; padding-right: 14px; margin-right: 2px; border-right: 1px solid var(--line); }
|
||||
.brand svg { width: 26px; height: 26px; filter: drop-shadow(0 0 10px rgba(141,125,255,.45)); }
|
||||
.brand-name { font-size: 15px; font-weight: 650; letter-spacing: -.03em; line-height: 1; color: #f2f4f9; }
|
||||
.brand-name em { font-style: normal; background: var(--accent-grad); -webkit-background-clip: text; background-clip: text; color: transparent; }
|
||||
.tool-group { display: flex; align-items: center; gap: 3px; }
|
||||
.tool, .metric {
|
||||
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; }
|
||||
@keyframes wiggle { 25% { transform: rotate(-9deg); } 75% { transform: rotate(7deg); } }
|
||||
@keyframes blink { 50% { opacity: .35; } }
|
||||
@keyframes barber { to { background-position: 28px 0; } }
|
||||
@keyframes plonk { from { opacity: 0; transform: translateY(-10px) rotate(-3deg); } }
|
||||
@keyframes bob { 50% { transform: rotate(4deg) translateY(-4px); } }
|
||||
|
||||
/* ---- Toolbar ---- */
|
||||
.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); }
|
||||
.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); }
|
||||
.brand { display: flex; gap: 9px; align-items: center; padding-right: 14px; border-right: 2px dashed 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); }
|
||||
.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); }
|
||||
|
||||
/* Toolbar buttons are labelled chips: icon + word, same height everywhere. */
|
||||
.tool-group { display: flex; align-items: center; gap: 4px; padding: 3px; border: 2px solid var(--black); border-radius: 10px; background: var(--ink); }
|
||||
.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; }
|
||||
.tool.icon-only { width: 36px; padding: 0; }
|
||||
.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; }
|
||||
.metric b { font: 700 13px/1 var(--mono); }
|
||||
.tool:hover, .metric:hover { color: var(--paper); background: var(--ink-3); }
|
||||
.tool.active, .metric.active { color: var(--black); border-color: var(--black); background: var(--lime); box-shadow: 2px 2px 0 var(--black); }
|
||||
.metric.active { background: var(--pink); }
|
||||
.tool.primary { color: var(--black); border-color: var(--black); background: var(--tang); box-shadow: 3px 3px 0 var(--black); transform: rotate(-2deg); }
|
||||
.tool.primary:hover { background: var(--yellow); transform: rotate(1deg) translate(-1px, -1px); box-shadow: 4px 4px 0 var(--black); }
|
||||
.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 small { display: block; margin-top: 3px; color: var(--dim); font-weight: 400; }
|
||||
.filter-wrap { position: relative; width: min(260px, 22vw); }
|
||||
.filter-wrap::before {
|
||||
content: "";
|
||||
position: absolute; left: 11px; top: 50%;
|
||||
width: 12px; height: 12px; transform: translateY(-50%);
|
||||
background: currentColor; color: var(--dim);
|
||||
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m20 20-3.5-3.5'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m20 20-3.5-3.5'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
pointer-events: none;
|
||||
}
|
||||
.filter-wrap input {
|
||||
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); } }
|
||||
.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; color: var(--dim); font-weight: 400; }
|
||||
.filter-wrap { position: relative; flex-shrink: 0; width: clamp(200px, 18vw, 270px); }
|
||||
.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; }
|
||||
.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; }
|
||||
.filter-wrap input::placeholder { color: var(--dim); font-style: italic; }
|
||||
.filter-wrap input:focus { border-color: var(--pink); box-shadow: 3px 3px 0 var(--pink); }
|
||||
.filter-wrap kbd { position: absolute; right: 9px; top: 50%; transform: translateY(-50%) rotate(4deg); }
|
||||
.live-dot { flex-shrink: 0; width: 12px; height: 12px; border: 2px solid var(--black); border-radius: 50%; background: var(--dim); }
|
||||
.live-dot.connected { background: var(--lime); animation: blink 1.6s steps(2, jump-none) infinite; }
|
||||
|
||||
/* Workspace and viewport */
|
||||
.workspace { position: absolute; inset: var(--toolbar-height) 0 0; display: grid; grid-template-columns: minmax(0,1fr) var(--panel-width); }
|
||||
.viewport {
|
||||
position: relative; min-width: 0; overflow: hidden; touch-action: none; user-select: none; cursor: grab;
|
||||
background:
|
||||
radial-gradient(1200px 700px at 30% -10%, rgba(141,125,255,.13), transparent 60%),
|
||||
radial-gradient(900px 600px at 90% 110%, rgba(54,211,194,.09), transparent 60%),
|
||||
radial-gradient(circle at 1px 1px, rgba(255,255,255,.045) 1px, transparent 0) 0 0 / 22px 22px,
|
||||
var(--bg);
|
||||
}
|
||||
/* ---- Workspace ---- */
|
||||
.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; }
|
||||
.workspace.inspector-hidden { grid-template-columns: minmax(0,1fr) 0px; }
|
||||
.workspace.inspector-hidden .inspector { border-left: 0; }
|
||||
.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); }
|
||||
.viewport.dragging { cursor: grabbing; }
|
||||
.viewport.panning { cursor: move; }
|
||||
#glCanvas, #overlayCanvas { position: absolute; inset: 0; width: 100%; height: 100%; }
|
||||
#overlayCanvas { pointer-events: none; }
|
||||
|
||||
.breadcrumb, .canvas-controls, .camera-help, .legend, .tooltip, .progress-card, .city-hud, .city-target, .city-address, .minimap, .tour-caption {
|
||||
border: 1px solid var(--line);
|
||||
background: var(--surface);
|
||||
-webkit-backdrop-filter: var(--glass);
|
||||
backdrop-filter: var(--glass);
|
||||
box-shadow: var(--shadow), inset 0 1px rgba(255,255,255,.05);
|
||||
}
|
||||
/* A tab stuck to the sidebar's left edge, half way down, that folds the sidebar away. */
|
||||
.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; }
|
||||
.inspector-toggle:hover { width: 28px; background: var(--yellow); }
|
||||
.inspector-toggle svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; transition: transform .22s; }
|
||||
.workspace.inspector-hidden .inspector-toggle { right: 0; }
|
||||
.workspace.inspector-hidden .inspector-toggle svg { transform: rotate(180deg); }
|
||||
|
||||
/* Welcome */
|
||||
.welcome { position: absolute; inset: 0; z-index: 1; display: grid; place-content: center; justify-items: start; padding: 32px; max-width: 760px; margin: 0 auto; cursor: default; }
|
||||
.welcome .eyebrow { display: inline-flex; align-items: center; gap: 8px; padding: 5px 11px 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: rgba(255,255,255,.03); color: var(--muted); font-size: 11px; font-weight: 500; }
|
||||
.welcome .eyebrow i { width: 7px; height: 7px; border-radius: 50%; background: var(--accent-grad); box-shadow: 0 0 10px rgba(141,125,255,.8); }
|
||||
.welcome h1 { margin: 20px 0 0; font-size: clamp(34px, 5.2vw, 60px); line-height: 1.02; font-weight: 700; letter-spacing: -.045em; color: #f4f6fb; }
|
||||
.welcome h1 span { background: var(--accent-grad); -webkit-background-clip: text; background-clip: text; color: transparent; }
|
||||
.welcome p { max-width: 520px; margin: 18px 0 0; color: var(--muted); font-size: 15px; line-height: 1.6; }
|
||||
.welcome-actions { display: flex; align-items: center; gap: 16px; margin-top: 30px; color: var(--dim); font-size: 11px; }
|
||||
.welcome-features { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 36px; }
|
||||
.welcome-features span { padding: 6px 11px; border: 1px solid var(--line); border-radius: 999px; background: rgba(255,255,255,.02); color: var(--muted); font-size: 11px; }
|
||||
.button-primary, .dialog-button, .github-row button {
|
||||
height: 40px; padding: 0 20px;
|
||||
border: 0; border-radius: 11px;
|
||||
color: #fff; background: var(--accent-grad);
|
||||
font-weight: 600; letter-spacing: -.005em; cursor: pointer;
|
||||
box-shadow: 0 8px 24px rgba(95,157,255,.32), inset 0 1px rgba(255,255,255,.28);
|
||||
transition: transform .15s, box-shadow .15s, filter .15s;
|
||||
}
|
||||
.button-primary:hover, .dialog-button:hover, .github-row button:hover { filter: brightness(1.08); transform: translateY(-1px); box-shadow: 0 12px 30px rgba(95,157,255,.42), inset 0 1px rgba(255,255,255,.28); }
|
||||
.button-primary:active, .dialog-button:active, .github-row button:active { transform: none; filter: brightness(.95); }
|
||||
/* Floating things on the map are stickers: flat, black-edged, hard shadow, never quite square. */
|
||||
.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); }
|
||||
|
||||
/* Inspector */
|
||||
.inspector { min-width: 0; overflow: hidden; background: linear-gradient(180deg, var(--bg-raised), var(--bg)); border-left: 1px solid var(--line); }
|
||||
.tabs { height: 46px; display: flex; align-items: center; gap: 2px; margin: 0 12px; padding: 0; border-bottom: 1px solid var(--line); }
|
||||
.tab { position: relative; height: 46px; padding: 0 10px; border: 0; background: transparent; color: var(--dim); font-weight: 500; cursor: pointer; }
|
||||
.tab:hover { color: var(--text); }
|
||||
.tab.active { color: #fff; }
|
||||
.tab.active::after { content: ""; position: absolute; left: 8px; right: 8px; bottom: -1px; height: 2px; border-radius: 2px; background: var(--accent-grad); box-shadow: 0 0 12px rgba(141,125,255,.7); }
|
||||
#resultBadge:not(:empty) { margin-left: 4px; padding: 1px 6px; border-radius: 999px; background: var(--accent-soft); color: #cfc8ff; font-size: 10px; font-weight: 600; }
|
||||
.panel { display: none; height: calc(100% - 46px); overflow: auto; padding: 16px 14px 24px; }
|
||||
/* ---- Welcome ---- */
|
||||
.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; }
|
||||
.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); }
|
||||
.welcome .eyebrow i { width: 8px; height: 8px; border-radius: 50%; background: var(--black); animation: blink 1s steps(2, jump-none) infinite; }
|
||||
.welcome h1 { position: relative; margin: 22px 0 0; font: 400 clamp(38px, 6vw, 72px)/.98 var(--display); color: var(--paper); }
|
||||
.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); }
|
||||
.welcome p { max-width: 540px; margin: 20px 0 0; color: var(--muted); font-size: 16px; line-height: 1.6; }
|
||||
.welcome p b { color: var(--paper); }
|
||||
.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-hint, .result-summary { margin: 0 0 14px; color: var(--muted); line-height: 1.6; }
|
||||
.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 h2 { margin: 5px 0 0; color: #f4f6fb; font-size: 19px; font-weight: 650; letter-spacing: -.035em; overflow-wrap: anywhere; }
|
||||
.meta-grid { display: grid; grid-template-columns: 1fr auto; gap: 0 12px; padding: 4px 12px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface-2); font-variant-numeric: tabular-nums; }
|
||||
.meta-grid span, .meta-grid b { padding: 7px 0; border-bottom: 1px solid var(--line); }
|
||||
.entity-title small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dim); font: 11px var(--mono); }
|
||||
.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: 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 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 { color: var(--muted); }
|
||||
.meta-grid b { color: var(--text); font-weight: 600; 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; }
|
||||
.relation { display: flex; gap: 9px; align-items: center; padding: 6px 8px; margin: 0 -8px; border-radius: 8px; cursor: pointer; }
|
||||
.relation:hover { background: var(--hover); }
|
||||
.relation i { width: 6px; height: 6px; border-radius: 50%; background: var(--accent-2); box-shadow: 0 0 8px rgba(54,211,194,.6); }
|
||||
.relation span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #cdd3df; }
|
||||
.relation small { color: var(--dim); font-size: 10px; }
|
||||
.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; }
|
||||
.alert-row:hover { background: var(--hover); }
|
||||
.alert-row em { padding: 2px 7px; border-radius: 999px; font: 700 9.5px var(--mono); font-style: normal; text-transform: uppercase; letter-spacing: .05em; }
|
||||
.meta-grid b { color: var(--paper); font: 700 12px var(--mono); text-align: right; }
|
||||
.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; }
|
||||
.section-title::before { content: ""; width: 9px; height: 9px; flex-shrink: 0; border: 2px solid var(--black); background: var(--pink); transform: rotate(45deg); }
|
||||
.relation { display: flex; gap: 9px; align-items: center; margin: 0 -8px; padding: 6px 8px; border-radius: 6px; cursor: pointer; }
|
||||
.relation:hover { background: var(--ink-3); }
|
||||
.relation i { width: 7px; height: 7px; border-radius: 50%; background: var(--lime); }
|
||||
.relation span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--paper); }
|
||||
.relation small { color: var(--dim); font: 10px var(--mono); }
|
||||
.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: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 { color: var(--text); font-weight: 500; }
|
||||
.alert-row small { color: var(--dim); font-size: 10.5px; }
|
||||
.severity-critical { color: #fff; background: #d7263d; }
|
||||
.severity-high { color: #1b0d00; background: #ff8a3d; }
|
||||
.severity-medium { color: #1b1400; background: #f5c518; }
|
||||
.severity-low { color: #cfd5e2; background: rgba(255,255,255,.12); }
|
||||
.alert-row strong { color: var(--paper); }
|
||||
.alert-row small { color: var(--dim); font: 10px var(--mono); }
|
||||
.severity-critical { color: var(--paper); background: var(--red); }
|
||||
.severity-high { color: var(--black); background: var(--tang); }
|
||||
.severity-medium { color: var(--black); background: var(--yellow); }
|
||||
.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 small, .coverage-list-head small, .source-title small { color: var(--dim); font-size: 10px; font-weight: 400; text-transform: none; letter-spacing: 0; }
|
||||
.coverage-menu { display: grid; gap: 2px; }
|
||||
.coverage-row { width: 100%; min-height: 34px; display: grid; grid-template-columns: 8px 1fr auto 12px; gap: 10px; align-items: center; padding: 5px 9px; border: 1px solid transparent; border-radius: 9px; background: transparent; color: #d6dbe6; text-align: left; cursor: pointer; }
|
||||
.coverage-row:hover { background: var(--hover); }
|
||||
.coverage-row.active { border-color: var(--accent-line); background: var(--accent-soft); }
|
||||
.coverage-row i { width: 8px; height: 8px; border-radius: 3px; box-shadow: 0 0 10px currentColor; }
|
||||
.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: 3px; }
|
||||
.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(--ink-3); }
|
||||
.coverage-row.active { color: var(--black); border-color: var(--black); background: var(--lime); box-shadow: 2px 2px 0 var(--black); }
|
||||
.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 small { color: var(--dim); font-variant-numeric: tabular-nums; }
|
||||
.coverage-row b { color: var(--dim); font-size: 14px; font-weight: 400; text-align: center; }
|
||||
.coverage-list-head { margin: 18px 0 8px; color: var(--text); font-weight: 600; }
|
||||
.coverage-file-list { position: relative; height: min(39vh, 350px); min-height: 150px; overflow: auto; border: 1px solid var(--line); border-radius: 12px; outline: none; background: rgba(0,0,0,.25); }
|
||||
.coverage-file-list:focus { border-color: var(--accent-line); box-shadow: 0 0 0 3px rgba(141,125,255,.14); }
|
||||
.coverage-row small { color: inherit; opacity: .7; font: 11px var(--mono); }
|
||||
.coverage-row b { font: 700 14px var(--mono); text-align: center; }
|
||||
.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: 2px solid var(--black); border-radius: 8px; outline: none; background: var(--ink); }
|
||||
.coverage-file-list:focus { border-color: var(--pink); }
|
||||
.coverage-list-spacer { width: 1px; }
|
||||
.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:hover { background: var(--hover); }
|
||||
.coverage-file.active { background: var(--accent-soft); box-shadow: inset 2px 0 var(--accent); }
|
||||
.coverage-file > i { border-radius: 2px; }
|
||||
.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(--ink-3); }
|
||||
.coverage-file.active { background: var(--ink-3); box-shadow: inset 4px 0 var(--pink); }
|
||||
.coverage-file > i { border-radius: 1px; }
|
||||
.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 { color: var(--text); font-weight: 500; }
|
||||
.coverage-file small { color: var(--dim); font-size: 10px; }
|
||||
.coverage-file em { align-self: center; color: var(--dim); font-size: 10px; font-style: normal; font-variant-numeric: tabular-nums; }
|
||||
.coverage-file strong { color: var(--paper); }
|
||||
.coverage-file small { color: var(--dim); font: 10px var(--mono); }
|
||||
.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; }
|
||||
.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:focus { border-color: var(--accent-line); box-shadow: 0 0 0 3px rgba(141,125,255,.12); }
|
||||
.code-preview { position: relative; height: min(46vh, 430px); min-height: 220px; overflow: auto; border: 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(--pink); }
|
||||
.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-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; }
|
||||
.tok-plain { color: #b6bfd0; }
|
||||
.tok-comment { color: #5c667a; font-style: italic; }
|
||||
.tok-string { color: #8fd694; }
|
||||
.tok-number { color: #f5a97f; }
|
||||
.tok-keyword { color: #c69cff; }
|
||||
.tok-type { color: #6ec6ff; }
|
||||
.tok-function { color: #7aa7ff; }
|
||||
.tok-property { color: #5fd3c6; }
|
||||
.tok-operator { color: #8f9ab0; }
|
||||
.tok-constant { color: #ff8fa3; }
|
||||
.tok-tag { color: #5fd3c6; }
|
||||
.result-row { display: grid; grid-template-columns: 3px minmax(0,1fr); gap: 10px; min-width: 0; margin: 0 -6px; padding: 8px 8px; border-radius: 9px; cursor: pointer; }
|
||||
.result-row:hover { background: var(--hover); }
|
||||
.result-row i { border-radius: 2px; background: var(--yellow); }
|
||||
.result-row strong { display: block; color: var(--text); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.result-row small { display: block; color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.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: #d9d0bd; }
|
||||
.tok-comment { color: #7d745f; font-style: italic; }
|
||||
.tok-string { color: #c8f135; }
|
||||
.tok-number { color: #ff9a1f; }
|
||||
.tok-keyword { color: #ff5ea8; }
|
||||
.tok-type { color: #5cc8ff; }
|
||||
.tok-function { color: #ffd23f; }
|
||||
.tok-property { color: #9be7c4; }
|
||||
.tok-operator { color: #a79d88; }
|
||||
.tok-constant { color: #ff7a6b; }
|
||||
.tok-tag { color: #5cc8ff; }
|
||||
.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(--ink-3); }
|
||||
.result-row i { border-radius: 1px; background: var(--tang); }
|
||||
.result-row strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--paper); }
|
||||
.result-row small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dim); font: 10px var(--mono); }
|
||||
|
||||
/* 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); }
|
||||
/* ---- Floating canvas chrome ---- */
|
||||
.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; }
|
||||
.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:hover { border-color: var(--accent-line); background: var(--accent-soft); }
|
||||
.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); }
|
||||
.canvas-controls { position: absolute; right: 16px; bottom: 16px; display: flex; gap: 2px; padding: 4px; border-radius: 12px; }
|
||||
.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:hover { background: var(--hover); color: #fff; }
|
||||
.canvas-controls svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; }
|
||||
.legend { position: absolute; right: 12px; top: 12px; display: none; width: 210px; padding: 6px; border-radius: 12px; }
|
||||
.legend.open { display: grid; gap: 2px; animation: rise .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:hover { background: var(--hover); }
|
||||
.legend button b { color: var(--dim); font-weight: 500; font-variant-numeric: tabular-nums; }
|
||||
.legend i { display: inline-block; width: 8px; height: 8px; margin-right: 8px; border-radius: 3px; }
|
||||
.tooltip { position: absolute; z-index: 3; max-width: 320px; padding: 8px 11px; pointer-events: none; border-radius: 10px; color: #fff; font-weight: 500; }
|
||||
.tooltip small { display: block; margin-top: 1px; color: var(--muted); font-weight: 400; }
|
||||
.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 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:hover { border-color: var(--accent-line); background: var(--accent-soft); }
|
||||
.camera-help b { color: var(--muted); font-weight: 600; }
|
||||
@keyframes rise { from { opacity: 0; transform: translateY(4px); } }
|
||||
.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 { transform: rotate(-12deg); }
|
||||
.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: 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: 6px; background: transparent; color: var(--paper); font: 700 18px var(--mono); cursor: pointer; }
|
||||
.canvas-controls button:hover { background: var(--lime); color: var(--black); }
|
||||
.canvas-controls svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
|
||||
.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: plonk .18s ease-out; }
|
||||
.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(--ink-3); }
|
||||
.legend button b { color: var(--dim); font: 11px var(--mono); }
|
||||
.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: 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: #5d5647; font: 400 10.5px var(--mono); }
|
||||
.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 { 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 { background: var(--sky); color: var(--black); }
|
||||
.camera-help b { color: var(--paper); }
|
||||
|
||||
.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-views { display: flex; gap: 2px; }
|
||||
.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-views button:hover { color: #fff; background: var(--hover); }
|
||||
.city-views button.active { color: #fff; border-color: var(--accent-line); background: var(--accent-soft); }
|
||||
.city-views kbd { padding: 0 4px; font-size: 9px; }
|
||||
.city-help b { color: var(--muted); font-weight: 600; }
|
||||
.city-actions { display: flex; gap: 2px; padding-left: 8px; border-left: 1px solid var(--line); }
|
||||
.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; }
|
||||
.city-actions button:hover { color: #fff; background: var(--hover); }
|
||||
.city-actions button.active { color: #fff; border-color: var(--accent-line); background: var(--accent-soft); }
|
||||
.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; }
|
||||
.tour-caption small { color: var(--accent-2); font: 600 10px var(--mono); letter-spacing: .12em; }
|
||||
.tour-caption strong { color: #fff; font-size: 22px; font-weight: 700; letter-spacing: -.03em; }
|
||||
.tour-caption span { color: var(--muted); font-size: 11.5px; }
|
||||
.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; }
|
||||
.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); }
|
||||
/* City bar: one row of identical chips (views, then actions), help text underneath. */
|
||||
.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-row { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
|
||||
.city-row .divider { width: 2px; height: 22px; margin: 0 4px; background: repeating-linear-gradient(var(--line) 0 4px, transparent 4px 7px); }
|
||||
.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; }
|
||||
.chip svg { width: 16px; height: 16px; flex-shrink: 0; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
||||
.chip kbd { margin-left: 1px; }
|
||||
.chip:hover { color: var(--paper); background: var(--ink-3); }
|
||||
.chip:hover svg { animation: wiggle .35s ease-in-out; }
|
||||
.chip.active { color: var(--black); border-color: var(--black); background: var(--lime); box-shadow: 2px 2px 0 var(--black); }
|
||||
#tourButton.active { background: var(--pink); }
|
||||
#shareButton.done { color: var(--black); border-color: var(--black); background: var(--sky); }
|
||||
.city-help { padding: 0 6px 2px; color: var(--dim); font: 10.5px var(--mono); }
|
||||
.city-help b { color: var(--paper); }
|
||||
.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; }
|
||||
.tour-caption small { padding: 0 6px; background: var(--pink); color: var(--black); font: 700 10px var(--mono); letter-spacing: .1em; }
|
||||
.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::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::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 small { display: block; color: var(--muted); font-weight: 400; font-size: 10.5px; }
|
||||
.city-target kbd { margin-left: 6px; }
|
||||
.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; }
|
||||
.minimap { position: absolute; z-index: 2; right: 16px; bottom: 62px; width: 220px; height: 150px; border-radius: 12px; cursor: crosshair; }
|
||||
.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); } }
|
||||
.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: #5d5647; font: 400 10.5px var(--mono); }
|
||||
.city-target kbd { margin-left: 6px; background: var(--lime); }
|
||||
.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: 6px; cursor: crosshair; transform: rotate(-1deg); }
|
||||
|
||||
.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 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); }
|
||||
#progressMessage { max-height: 100px; overflow: auto; overflow-wrap: anywhere; }
|
||||
.progress-track { position: relative; height: 4px; margin: 13px 0 7px; overflow: hidden; border-radius: 999px; background: rgba(255,255,255,.07); }
|
||||
.progress-track i { position: relative; display: block; height: 100%; width: 5%; overflow: hidden; border-radius: inherit; background: var(--accent-grad); box-shadow: 0 0 12px rgba(95,157,255,.6); transition: width .25s ease; }
|
||||
.progress-track i::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, transparent, rgba(255,255,255,.55), transparent); transform: translateX(-100%); animation: shimmer 1.4s ease-in-out infinite; }
|
||||
@keyframes shimmer { to { transform: translateX(100%); } }
|
||||
.progress-count { font-size: 10.5px; font-variant-numeric: tabular-nums; }
|
||||
#progressMessage { max-height: 100px; overflow: auto; overflow-wrap: anywhere; font: 11px var(--mono); }
|
||||
.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 { 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-count { font: 10.5px var(--mono); }
|
||||
.progress-dismiss { position: absolute; right: 8px; top: 8px; }
|
||||
.progress-action { display: inline-grid; place-items: center; width: fit-content; height: 36px; margin-top: 14px; text-decoration: none; }
|
||||
.empty-state { position: absolute; inset: 0; display: grid; place-content: center; gap: 4px; text-align: center; color: var(--text); }
|
||||
.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(--paper); }
|
||||
.empty-state strong { font: 400 20px var(--display); }
|
||||
.empty-state span { color: var(--muted); }
|
||||
|
||||
/* Dialogs */
|
||||
dialog {
|
||||
width: min(580px, calc(100vw - 30px));
|
||||
max-height: calc(100dvh - 32px);
|
||||
padding: 0;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 22px;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(600px 240px at 0% 0%, rgba(141,125,255,.16), transparent 70%),
|
||||
radial-gradient(500px 260px at 100% 100%, rgba(54,211,194,.08), transparent 70%),
|
||||
#0e1118;
|
||||
box-shadow: 0 40px 140px rgba(0,0,0,.7), inset 0 1px rgba(255,255,255,.08);
|
||||
}
|
||||
dialog[open] { animation: pop .22s cubic-bezier(.2,.9,.3,1.2); }
|
||||
@keyframes pop { from { opacity: 0; transform: scale(.97) translateY(6px); } }
|
||||
dialog::backdrop { background: rgba(3,4,8,.62); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); }
|
||||
.dialog-shell { padding: 30px; }
|
||||
/* ---- Dialogs ---- */
|
||||
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); }
|
||||
dialog[open] { animation: plonk .2s ease-out; transform: rotate(-.4deg); }
|
||||
dialog::backdrop { background: rgba(10, 8, 5, .78); }
|
||||
.dialog-shell { padding: 28px; }
|
||||
.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 h1 { margin: 8px 0 0; font-size: 28px; font-weight: 700; letter-spacing: -.045em; color: #f4f6fb; }
|
||||
.dialog-description { margin: 8px 0 2px; color: var(--muted); font-size: 13px; }
|
||||
.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 svg { display: block; width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; }
|
||||
.close-button:hover { border-color: var(--line); background: var(--hover); color: #fff; }
|
||||
.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; }
|
||||
.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: 10px 0 0; font: 400 30px/1.05 var(--display); color: var(--paper); }
|
||||
.dialog-description { margin: 8px 0 2px; color: var(--muted); font-size: 14px; }
|
||||
.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: 2.4; stroke-linecap: round; }
|
||||
.close-button:hover { background: var(--red); color: var(--black); transform: rotate(90deg); }
|
||||
.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:hover { transform: translateY(-1px); border-color: var(--accent-line); background: rgba(141,125,255,.07); box-shadow: 0 10px 30px rgba(0,0,0,.3); }
|
||||
.source-card:focus-within:not(button) { border-color: rgba(255,255,255,.14); }
|
||||
.source-card > b { margin-left: auto; color: var(--dim); font-size: 18px; transition: transform .18s, color .18s; }
|
||||
button.source-card:hover > b { transform: translateX(3px); color: #fff; }
|
||||
.source-icon { width: 40px; height: 40px; display: grid; place-items: center; border: 1px solid rgba(141,125,255,.35); border-radius: 12px; color: #d9d2ff; background: linear-gradient(135deg, rgba(141,125,255,.22), rgba(54,211,194,.1)); }
|
||||
.source-icon svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
|
||||
button.source-card:hover { box-shadow: 5px 5px 0 var(--sky); transform: translate(-2px, -2px); }
|
||||
.github-card:focus-within { box-shadow: 5px 5px 0 var(--lime); }
|
||||
.source-card > b { margin-left: auto; color: var(--dim); font: 700 20px var(--mono); transition: transform .12s, color .12s; }
|
||||
button.source-card:hover > b { color: var(--sky); transform: translateX(4px); }
|
||||
.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); }
|
||||
.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 { color: #f1f3f8; font-size: 13.5px; font-weight: 600; }
|
||||
.source-card small { margin-top: 2px; color: var(--muted); font-weight: 400; }
|
||||
.github-card { display: grid; grid-template-columns: 40px 1fr; gap: 0 14px; align-items: center; }
|
||||
.github-row { grid-column: 1 / -1; display: flex; gap: 8px; 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; }
|
||||
.source-card strong { font: 400 15px var(--display); color: var(--paper); }
|
||||
.source-card small { margin-top: 2px; color: var(--muted); }
|
||||
.github-card { display: grid; grid-template-columns: 42px 1fr; gap: 0 14px; align-items: center; }
|
||||
.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 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:focus { border-color: var(--accent-line); box-shadow: 0 0 0 4px rgba(141,125,255,.14); }
|
||||
.github-row button { height: 42px; }
|
||||
.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; 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 { color: #fff; border-bottom-color: #fff; }
|
||||
.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-color: rgba(54,211,160,.5); background: rgba(54,211,160,.1); }
|
||||
.github-notice[data-tone="warn"] { border-color: rgba(242,201,76,.5); background: rgba(242,201,76,.1); }
|
||||
.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(--danger); font-size: 11px; }
|
||||
.dialog-foot { display: flex; gap: 8px; align-items: center; margin: 16px 2px 0; color: var(--dim); font-size: 11px; line-height: 1.6; }
|
||||
.dialog-foot::before { content: ""; flex-shrink: 0; width: 6px; height: 6px; border-radius: 50%; background: var(--green); box-shadow: 0 0 8px rgba(54,211,160,.7); }
|
||||
.github-row input:focus { border-color: var(--lime); }
|
||||
.github-account { grid-column: 1 / -1; margin: 10px 0 0; color: var(--muted); font-size: 12.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:hover, .github-account button:hover { background: var(--pink); color: var(--black); }
|
||||
.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[data-tone="ok"] { border-left-color: var(--lime); }
|
||||
.github-notice[data-tone="warn"] { border-left-color: var(--yellow); }
|
||||
.github-notice[data-tone="error"] { border-left-color: var(--red); }
|
||||
.form-error { grid-column: 1 / -1; min-height: 16px; margin: 6px 0 -6px; color: var(--red); font: 700 11px var(--mono); }
|
||||
.dialog-foot { display: flex; gap: 8px; align-items: center; margin: 16px 2px 0; color: var(--dim); font: 11px/1.6 var(--mono); }
|
||||
.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; }
|
||||
.settings-dialog { width: min(480px, calc(100vw - 30px)); }
|
||||
.settings-shell { padding: 28px; }
|
||||
.settings-status { display: flex; align-items: center; justify-content: space-between; margin: -2px 0 8px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 12px; background: rgba(0,0,0,.25); }
|
||||
.settings-status span { display: flex; align-items: center; gap: 8px; color: var(--text); font-weight: 600; }
|
||||
.settings-status i { width: 7px; height: 7px; border-radius: 50%; background: var(--green); box-shadow: 0 0 8px rgba(54,211,160,.7); }
|
||||
.settings-status small { color: var(--dim); font-size: 10px; letter-spacing: .08em; }
|
||||
.preference-row { display: flex; gap: 16px; justify-content: space-between; align-items: center; padding: 16px 0; border-bottom: 1px solid var(--line); }
|
||||
.preference-row label { color: var(--text); font-weight: 500; }
|
||||
.preference-row small { display: block; margin-top: 3px; color: var(--muted); font-size: 11px; font-weight: 400; }
|
||||
.preference-row select { padding: 8px 10px; border: 1px solid var(--line-strong); border-radius: 10px; background: rgba(0,0,0,.35); color: var(--text); }
|
||||
.switch { appearance: none; flex-shrink: 0; width: 38px; height: 22px; margin: 0; padding: 2px; border: 1px solid var(--line-strong); border-radius: 999px; background: rgba(255,255,255,.06); cursor: pointer; transition: background .2s, border-color .2s; }
|
||||
.switch::before { content: ""; display: block; width: 16px; height: 16px; border-radius: 50%; background: #aab2c3; transition: transform .2s cubic-bezier(.3,1.3,.5,1), background .2s; }
|
||||
.switch:checked { border-color: transparent; background: var(--accent-grad); }
|
||||
.switch:checked::before { background: #fff; transform: translateX(16px); }
|
||||
.shortcut-guide { margin-top: 18px; color: var(--muted); font-size: 11px; }
|
||||
.settings-dialog[open] { transform: rotate(.5deg); }
|
||||
.settings-shell { padding: 26px; }
|
||||
.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 span { display: flex; align-items: center; gap: 8px; color: var(--paper); font-weight: 700; }
|
||||
.settings-status i { width: 9px; height: 9px; border: 2px solid var(--black); border-radius: 50%; background: var(--lime); }
|
||||
.settings-status small { color: var(--dim); font: 700 10px var(--mono); letter-spacing: .08em; }
|
||||
.preference-row { display: flex; gap: 16px; justify-content: space-between; align-items: center; padding: 15px 0; border-bottom: 1px dashed var(--line); }
|
||||
.preference-row label { color: var(--paper); font-weight: 700; }
|
||||
.preference-row small { display: block; margin-top: 3px; color: var(--muted); font-size: 11.5px; font-weight: 400; }
|
||||
.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 { 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::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 { background: var(--lime); }
|
||||
.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 div { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; }
|
||||
.settings-note { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; margin-top: 16px; padding: 12px; border: 1px solid var(--line); border-radius: 14px; background: rgba(255,255,255,.025); }
|
||||
.settings-note-icon { width: 34px; height: 34px; display: grid; place-items: center; border-radius: 10px; color: #d9d2ff; background: var(--accent-soft); font-size: 16px; }
|
||||
.settings-note { 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: 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 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-actions { display: flex; justify-content: flex-end; margin-top: 18px; }
|
||||
.dialog-button { min-width: 90px; }
|
||||
.settings-actions { display: flex; justify-content: flex-end; margin-top: 20px; }
|
||||
.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; }
|
||||
|
||||
@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) {
|
||||
:root { --panel-width: 260px; }
|
||||
.metric span, .brand-name { display: none; }
|
||||
.brand { padding-right: 10px; }
|
||||
.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) {
|
||||
:root { --panel-width: 0px; }
|
||||
.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); }
|
||||
.tabs { height: 40px; }
|
||||
.tab { height: 40px; }
|
||||
.panel { height: calc(100% - 40px); padding: 10px 12px; }
|
||||
.separator, #layersButton, #codeButton, .palette, .brand, .metric-switch, #mapButton, .toolbar-spacer { display: none; }
|
||||
.inspector { display: block; border-left: 0; border-top: 2px solid var(--black); }
|
||||
.inspector > * { min-width: 0; }
|
||||
.inspector-toggle { display: none; }
|
||||
.tabs { height: 44px; }
|
||||
.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; }
|
||||
.dialog-shell { padding: 22px; }
|
||||
.dialog-shell { padding: 20px; }
|
||||
.welcome { padding: 20px; }
|
||||
.welcome-features { display: none; }
|
||||
.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-address { left: 10px; top: 58px; }
|
||||
.breadcrumb { left: 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; } }
|
||||
|
||||
Reference in New Issue
Block a user