﻿/*
 * App shell.
 *
 * Four regions: a thin top bar carrying the wordmark and the "runs locally"
 * badge; a three-column body of shrink controls / viewport / scene; a status
 * bar; and the diagnostics drawer.
 *
 * THE TWO SIDE PANELS ANSWER DIFFERENT QUESTIONS AND NOTHING CROSSES OVER.
 * Left is the shrink — four numbered steps that are the workflow. Right is the
 * scene, split into two tabs because it holds two kinds of thing: readings you
 * consult (Info) and controls you operate (Look). Stacking those was what made
 * it read as a pile.
 *
 * The drawer is a GRID ROW, not an overlay. Opening it shortens the viewport
 * rather than covering it, so nothing you were looking at hides behind the
 * thing you opened to read.
 */

*, *::before, *::after { box-sizing: border-box; }

html, body {
	margin: 0;
	height: 100%;
	background: var(--bg);
	color: var(--text);
	font-family: var(--font-ui);
	font-size: var(--fs-base);
	line-height: 1.45;
	-webkit-font-smoothing: antialiased;
}

.app {
	display: grid;
	height: 100dvh;
	overflow: hidden;
	grid-template-columns: var(--panel-width) minmax(0, 1fr) var(--results-width);
	/* The log row is `auto`, so it costs exactly zero height while hidden. */
	grid-template-rows: auto minmax(0, 1fr) auto auto;
	grid-template-areas:
		"top     top      top"
		"panel   view     scene"
		"status  status   status"
		"log     log      log";
}

/* --------------------------------------------------------------- top bar */

.topbar {
	grid-area: top;
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
	height: var(--topbar-h);
	padding: 0 var(--pad-x);
	background: var(--panel);
	border-bottom: 1px solid var(--line-soft);
}

.brand {
	display: flex;
	align-items: center;
	gap: 7px;
	text-decoration: none;
	color: var(--pink);
	width: fit-content;
}
.brand:hover { color: var(--pink-hover); }
.brand:focus-visible { outline: 2px solid var(--pink); outline-offset: 3px; border-radius: 3px; }

.brand-mark { width: 24px; height: 24px; flex: none; display: block; }

.brand-name {
	font-family: var(--font-display);
	font-weight: 900;
	font-size: var(--fs-brand);
	line-height: 1;
	letter-spacing: -0.018em;
	text-transform: lowercase;
	/* Skew rather than font-style: italic. Arial Black has no true italic, so
	 * the browser would synthesise one anyway; doing it explicitly pins the
	 * angle instead of letting it vary by platform.
	 *
	 * 9 DEGREES IS SHARED WITH THE MARK, which leans by rotate(9) in the SVG —
	 * and the two signs disagree on purpose. CSS skewX takes a negative angle to
	 * lean right; SVG rotate takes a positive one, because its y axis runs
	 * downward. Change one and you must change the other, in the opposite
	 * direction. (For a long time this comment claimed the angle matched a mark
	 * that had no tilt at all.) */
	transform: skewX(-9deg);
	transform-origin: left center;
	padding-right: 3px;
}

/* ------------------------------------------------------- runs-locally badge */

.badge-wrap { position: relative; }

.badge {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	height: var(--row-h-sm);
	padding: 0 11px;
	background: var(--control);
	border: 1px solid var(--line);
	border-radius: 999px;
	color: var(--text);
	font: inherit;
	font-size: var(--fs-small);
	cursor: help;
}
.badge:hover { background: var(--control-hover); }
.badge:focus-visible { outline: 2px solid var(--pink); outline-offset: 2px; }

.badge-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--pink); flex: none; }

/*
 * Anchored to the right edge, because the badge sits at the right of the window
 * and a left-anchored panel hangs off the side of the screen.
 *
 * The descendant selector is doing real work: controls.css sets
 * `.reference-panel { left: 0 }` and is loaded AFTER this file, so a
 * single-class override here loses on source order and silently does nothing.
 * Measured at a 700px viewport it put the panel's right edge 152px past the
 * window. Do not flatten this back to `.badge-panel`.
 */
.badge-wrap .badge-panel {
	left: auto;
	right: 0;
	width: min(296px, calc(100vw - 24px));
}
.badge-claim { margin: 0 0 10px; color: var(--text); }

/* ----------------------------------------------------------------- panels */

.panel, .scene {
	min-height: 0;
	background: var(--panel);
	overflow-y: auto;
	overscroll-behavior: contain;
}
.panel { grid-area: panel; border-right: 1px solid var(--line-soft); }
.scene { grid-area: scene; border-left: 1px solid var(--line-soft); display: flex; flex-direction: column; }

.section {
	padding: var(--pad-y) var(--pad-x);
	border-bottom: 1px solid var(--line-soft);
}
.section[hidden] { display: none; }

/* Level 1 of the panel hierarchy — see the note in tokens.css. */
.section-title {
	display: flex;
	align-items: center;
	gap: 8px;
	margin: 0 0 11px;
	font-size: var(--fs-step);
	font-weight: 600;
	letter-spacing: -0.005em;
	color: var(--text);
}

/* Numbered so the column reads as a sequence of steps rather than a pile of
 * settings. Small — it is an ordinal, not a badge of honour. */
.section-step {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 16px;
	height: 16px;
	border-radius: 4px;
	/* --pink-solid, not --pink: this is small text on a pink fill. See tokens.css. */
	background: var(--pink-solid);
	color: var(--pink-ink);
	font-size: var(--fs-micro);
	font-weight: 700;
	flex: none;
}

.format-list {
	margin: 7px 0 0;
	font-family: var(--font-mono);
	font-size: var(--fs-micro);
	letter-spacing: 0.04em;
	color: var(--text-dim);
}

.filename {
	display: block;
	margin: 8px 0 0;
	padding: 0 9px;
	height: var(--row-h);
	/* Minus the two 1px borders, so it lands on exactly --row-h like every other
	 * control rather than two pixels taller than all of them. */
	line-height: calc(var(--row-h) - 2px);
	background: var(--control);
	border: 1px solid var(--line-soft);
	border-radius: var(--radius);
	font-family: var(--font-mono);
	font-size: var(--fs-small);
	color: var(--text);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* --------------------------------------------------------- scene column */

.scene-body { padding: 2px var(--pad-x) 18px; }

/* ------------------------------------------------------- readouts, shared */

/*
 * The four figures that can be taken in without reading. Used by both Model
 * and Result on purpose — two ways of showing a triangle count in the same
 * column would read as two different apps.
 */
.statgrid {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: 1px;
	margin: 2px 0 10px;
	background: var(--line-soft);
	border: 1px solid var(--line-soft);
	border-radius: var(--radius);
	overflow: hidden;
}

.stat {
	display: flex;
	flex-direction: column;
	gap: 1px;
	padding: 7px 9px;
	background: var(--panel);
}

.stat-value {
	font-family: var(--font-mono);
	font-size: 13px;
	font-variant-numeric: tabular-nums;
	color: var(--text);
	line-height: 1.2;
}

.stat-label {
	font-size: var(--fs-micro);
	letter-spacing: 0.05em;
	text-transform: uppercase;
	color: var(--text-dim);
}

/* Small tags for yes/no facts. A missing chip is as informative as a present
 * one, which is why they are never rendered greyed out. */
.chiprow { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 10px; }
.chiprow:empty { display: none; }

.chip {
	display: inline-flex;
	align-items: center;
	gap: 4px;
	padding: 2px 7px;
	border: 1px solid var(--line-soft);
	border-radius: 999px;
	background: var(--control);
	color: var(--text-dim);
	font-size: var(--fs-micro);
	white-space: nowrap;
}
/* Only a problem gets a hue. See the colour rule in tokens.css. */
.chip[data-level="warn"] { color: var(--warn); border-color: var(--warn); }
.chip-quiet { border-style: dashed; }

.kv { display: flex; flex-direction: column; }

.kv-row {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: 10px;
	padding: 3px 0;
	font-size: var(--fs-small);
}
.kv-label { color: var(--text-dim); }
.kv-value {
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	color: var(--text);
	text-align: right;
}

.note {
	margin: 9px 0 0;
	font-size: var(--fs-small);
	line-height: 1.5;
	color: var(--text-dim);
}

/* --------------------------------------------------------------- textures */

.texlist { display: flex; flex-direction: column; gap: 10px; }

.texitem {
	display: grid;
	grid-template-columns: auto 1fr;
	gap: 9px;
	align-items: start;
}
.texbody { min-width: 0; }
.texitem strong {
	display: block;
	color: var(--text);
	font-weight: 600;
	font-size: var(--fs-base);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}
.texmeta {
	margin: 1px 0 4px;
	color: var(--text-dim);
	font-family: var(--font-mono);
	font-size: var(--fs-micro);
}

.texthumb {
	width: 38px;
	height: 38px;
	border-radius: 4px;
	/* A hairline so a white or fully transparent map still reads as a swatch
	 * rather than as a hole in the panel. */
	border: 1px solid var(--line-soft);
	flex: none;
	display: block;
}

/* --------------------------------------------------------------- findings */

.findings { display: flex; flex-direction: column; gap: 11px; }

.finding {
	display: grid;
	grid-template-columns: auto 1fr;
	gap: 9px;
	font-size: var(--fs-small);
	line-height: 1.5;
}
.finding strong { display: block; color: var(--text); font-weight: 600; font-size: var(--fs-base); }
.finding p { margin: 2px 0 0; color: var(--text-dim); }

/*
 * ok and info are NEUTRAL. The icon shape carries the severity; only a warning
 * spends a colour, because only a warning needs interrupting for. See tokens.css.
 */
.finding-icon { display: flex; margin-top: 1px; color: var(--text-dim); }
.finding[data-level="warn"] .finding-icon { color: var(--warn); }

/* --------------------------------------------------------------- viewport */

.viewport {
	grid-area: view;
	position: relative;
	min-width: 0;
	background: var(--viewport);
}

.viewport-canvas { position: absolute; inset: 0; }
.viewport-canvas canvas { display: block; width: 100%; height: 100%; }

/*
 * Floating glass.
 *
 * The alpha floor is documented on --glass in tokens.css and is not a style
 * choice — go below it and these become unreadable over a light background
 * while still looking fine over the default grey.
 *
 * THE RIM IS A LIGHT HIGHLIGHT AND IS TREATED AS DECORATIVE. That is a
 * deliberate call, not an oversight. These panels float over a backdrop the
 * user chooses, which ranges from near-white to near-black, and NO single
 * border colour clears 3:1 against both ends of that range — measured, the
 * previous solid --line rim managed 3.21:1 over the dark options and 1.55:1
 * over white. What identifies these components is their own content: every
 * label on them clears 7:1 through the tint (7.66:1 on the worst backdrop) and
 * the active segment carries a 7.08:1 pink fill. The rim and the shadow are
 * craft, and the shadow is what actually separates a floating element from a
 * light background.
 */
.glass {
	background: var(--glass);
	border: 1px solid rgba(255, 255, 255, 0.14);
	border-radius: var(--radius);
	box-shadow: 0 4px 18px rgba(0, 0, 0, 0.42);
	backdrop-filter: var(--glass-blur);
	-webkit-backdrop-filter: var(--glass-blur);
}

.viewport-chrome {
	position: absolute;
	top: 10px;
	left: 10px;
	right: 10px;
	display: flex;
	flex-wrap: wrap;
	gap: 6px;
	align-items: flex-start;
	pointer-events: none;
}
.viewport-chrome > * { pointer-events: auto; }
.viewport-chrome[hidden] { display: none; }

.toolbar-spacer { flex: 1 1 auto; pointer-events: none; }

.toolbar-group { display: flex; align-items: center; gap: 2px; padding: 3px; }

/* -------------------------------------------------------- render popover */

.popover-wrap { position: relative; }

/* A button that looks like the rest of the floating glass, but behaves like a
 * control: click to open, click away or Escape to close. */
.chip-button {
	height: var(--row-h);
	padding: 0 12px;
	color: var(--text);
	font: inherit;
	font-size: var(--fs-small);
	cursor: pointer;
}
.chip-button:hover { background: rgba(11, 11, 13, 0.86); }
.chip-button.is-active { border-color: var(--pink); }
.chip-button:focus-visible { outline: 2px solid var(--pink); outline-offset: 2px; }

/*
 * Solid, not glass. It holds sliders and selects that have to be read
 * precisely, and reading a value through a blurred photograph of a 3D model is
 * exactly as bad as it sounds.
 */
.popover {
	position: absolute;
	z-index: 30;
	top: calc(100% + 7px);
	left: 0;
	width: 250px;
	max-height: 68vh;
	overflow-y: auto;
	padding: 4px 13px 12px;
	background: var(--panel);
	border: 1px solid var(--line);
	border-radius: var(--radius);
	box-shadow: 0 12px 34px rgba(0, 0, 0, 0.6);
}
.popover[hidden] { display: none; }

/*
 * TEXT ON GLASS IS ALWAYS --text, NEVER --text-dim.
 *
 * The overlays sit on whatever background the user picked, and one option is
 * near-white. Composited through the 0.72 tint, body text clears 7:1 on every
 * background (7.70:1 on the worst) but dimmed text only reaches 3.66:1 — it
 * would look perfectly fine on the default grey and be unreadable the moment
 * someone switched the backdrop. Holding dim text to the same bar would force
 * the tint back up to 0.85 alpha and lose the transparency entirely.
 *
 * Hierarchy inside these pills comes from weight, not colour.
 */
.stat-pill {
	display: flex;
	align-items: center;
	gap: 11px;
	height: var(--row-h);
	padding: 0 11px;
	font-family: var(--font-mono);
	font-size: var(--fs-small);
	color: var(--text);
	font-weight: 400;
}
.stat-pill b { font-weight: 700; font-variant-numeric: tabular-nums; }

.viewport-hint {
	position: absolute;
	left: 50%;
	bottom: 12px;
	transform: translateX(-50%);
	display: flex;
	align-items: center;
	gap: 4px;
	margin: 0;
	padding: 0 4px 0 14px;
	height: var(--row-h);
	border-radius: 999px;
	font-size: var(--fs-small);
	/* --text, not --text-dim — see the note on .stat-pill above. */
	color: var(--text);
	white-space: nowrap;
}
.viewport-hint[hidden] { display: none; }

/*
 * Once retired, the hint comes back when the pointer nears the bottom edge of
 * the viewport — it reappears exactly where it used to live, so it is easy to
 * rediscover, and costs nothing while the pointer is elsewhere.
 *
 * Done with a mousemove test in main.js rather than a hover zone element. An
 * absolutely positioned strip across the bottom would need pointer events to
 * register :hover, and would then swallow every orbit drag that started in the
 * lower part of the view.
 */
.viewport-hint.is-peeking { opacity: 0.92; }

.hint-close {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 20px;
	height: 20px;
	background: none;
	border: none;
	border-radius: 50%;
	color: var(--text);
	font-size: 15px;
	line-height: 1;
	cursor: pointer;
}
.hint-close:hover { background: rgba(255, 255, 255, 0.14); }
.hint-close:focus-visible { outline: 2px solid var(--pink); outline-offset: 1px; }

/* ------------------------------------------------------------- drop zone */

.dropzone {
	position: absolute;
	inset: 0;
	display: grid;
	place-content: center;
	gap: 6px;
	text-align: center;
	background: rgba(11, 11, 13, 0.55);
	border: 2px dashed var(--line);
	color: var(--text);
	font-size: 15px;
	opacity: 0;
	pointer-events: none;
	transition: opacity 120ms ease;
}
.dropzone.is-active {
	opacity: 1;
	border-color: var(--pink);
	background: rgba(255, 45, 149, 0.14);
}
.dropzone small { color: var(--text-dim); font-size: var(--fs-base); }

/* -------------------------------------------------------------- progress */

.progress {
	position: absolute;
	left: 50%;
	bottom: 52px;
	transform: translateX(-50%);
	width: min(400px, calc(100% - 32px));
	padding: 10px 13px;
	border-radius: var(--radius-lg);
}
.progress[hidden] { display: none; }

.progress-label {
	display: flex;
	justify-content: space-between;
	gap: 12px;
	margin-bottom: 7px;
	font-size: var(--fs-small);
	color: var(--text);
}
/* Full-strength --text, and no opacity either: fading it to 0.85 over the glass
 * drops it to 6.2:1 on a white backdrop, which is the same failure by another
 * route. See the note on .stat-pill. */
.progress-percent { color: var(--text); font-variant-numeric: tabular-nums; }

.progress-track { height: 4px; border-radius: 2px; background: var(--control); overflow: hidden; }
.progress-fill { height: 100%; width: 0%; background: var(--pink); transition: width 90ms linear; }

/* ------------------------------------------------------------- status bar */

.statusbar {
	grid-area: status;
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 5px var(--pad-x);
	background: var(--panel);
	border-top: 1px solid var(--line-soft);
	font-size: var(--fs-small);
}

.status-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--text-dim); flex: none; }
/* Pink is the app's own signal — running, finished. Severity uses its own two
 * hues and never borrows the brand colour. */
.status-dot[data-state="busy"] { background: var(--pink); animation: pulse 1s ease-in-out infinite; }
.status-dot[data-state="done"] { background: var(--pink); }
.status-dot[data-state="warn"] { background: var(--warn); }
.status-dot[data-state="error"] { background: var(--danger); }

@keyframes pulse { 50% { opacity: 0.3; } }

.status-text { color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.statusbar .button.is-active { border-color: var(--pink); color: var(--text); }

/* Count badge on the Findings button. Inset dark rather than the button's own
 * hover grey: dimmed text on --control-hover measures 6.39:1 and misses the 7:1
 * bar, where the same text on --bg is a comfortable 8.36:1. */
.pill-count {
	display: inline-block;
	min-width: 15px;
	padding: 0 4px;
	border-radius: 7px;
	background: var(--bg);
	color: var(--text-dim);
	font-size: var(--fs-micro);
	font-variant-numeric: tabular-nums;
	text-align: center;
}
[data-alert="true"] .pill-count { background: var(--warn); color: var(--pink-ink); }

/* ---------------------------------------------------------------- tooltip */

/*
 * position: fixed is load-bearing — see the note at the top of ui/tooltip.js.
 * An absolutely positioned tooltip inside a scrolling panel counts towards that
 * panel's scroll extent and invents a scrollbar on content that fits.
 */
.tooltip {
	position: fixed;
	z-index: 60;
	max-width: 264px;
	padding: 8px 10px;
	background: var(--bg);
	border: 1px solid var(--line);
	border-radius: var(--radius);
	box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
	color: var(--text);
	font-size: var(--fs-small);
	line-height: 1.5;
	pointer-events: none;
}
.tooltip[hidden] { display: none; }

/* --------------------------------------------------------------- drawer */

.drawer {
	grid-area: log;
	display: flex;
	/* Row, so two open panels sit side by side rather than stacking. */
	flex-direction: row;
	background: var(--bg);
	border-top: 1px solid var(--line-soft);
	/* Fixed height rather than a fraction of the window: this is reference
	 * material, and letting it grow with the window steals space from the
	 * viewport, which is the thing actually being looked at. */
	height: 200px;
}
.drawer[hidden] { display: none; }

/*
 * The two panels are independent and can both be open, in which case the drawer
 * splits side by side. There is no tab bar: the status-bar buttons are the
 * control, and a second row of tabs saying the same two words was both
 * redundant and — in the previous version — in the opposite order.
 */
.drawer-panel {
	flex: 1 1 0;
	min-width: 0;
	min-height: 0;
	display: flex;
	flex-direction: column;
}
.drawer-panel[hidden] { display: none; }
/* A divider only where two panels actually meet. */
.drawer-panel + .drawer-panel:not([hidden]) { border-left: 1px solid var(--line-soft); }

.drawer-heading {
	flex: none;
	margin: 0;
	padding: 7px var(--pad-x) 6px;
	border-bottom: 1px solid var(--line-soft);
	font-size: var(--fs-section);
	font-weight: 700;
	letter-spacing: 0.09em;
	text-transform: uppercase;
	color: var(--text-dim);
}
/* With only one panel open the heading duplicates the pressed button beside it;
 * it earns its place only when there are two panels to tell apart. */
.drawer:has(.drawer-panel:not([hidden]) + .drawer-panel[hidden]) .drawer-heading,
.drawer:has(.drawer-panel[hidden] + .drawer-panel:not([hidden])) .drawer-heading { display: none; }

.drawer-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 11px var(--pad-x); }

.log {
	height: 100%;
	overflow-y: auto;
	padding: 8px var(--pad-x);
	font-family: var(--font-mono);
	font-size: var(--fs-small);
	line-height: 1.7;
}

.log-line { display: flex; gap: 10px; }
.log-time { color: var(--text-dim); flex: none; font-variant-numeric: tabular-nums; }
.log-text { color: var(--text); word-break: break-word; }
/* Only the two states that need interrupting for get a colour. `ok` and
 * `muted` stay neutral — see tokens.css. */
.log-line[data-level="warn"] .log-text { color: var(--warn); }
.log-line[data-level="error"] .log-text { color: var(--danger); }
.log-line[data-level="ok"] .log-text { color: var(--text); }
.log-line[data-level="muted"] .log-text { color: var(--text-dim); }

/* --------------------------------------------------------- boot overlay */

#boot {
	position: fixed;
	inset: 0;
	z-index: 50;
	display: grid;
	place-content: center;
	justify-items: center;
	gap: 14px;
	padding: 24px;
	text-align: center;
	background: var(--bg);
}
#boot[hidden] { display: none; }
#boot .boot-name {
	font-family: var(--font-display);
	font-weight: 900;
	font-size: 30px;
	color: var(--pink);
	text-transform: lowercase;
	transform: skewX(-9deg);
	letter-spacing: -0.018em;
}
#boot .boot-status { color: var(--text-dim); font-size: var(--fs-base); max-width: 46ch; }
#boot .boot-status a { color: var(--pink-text); }

/* ------------------------------------------------------------- responsive */

/*
 * The viewport keeps its width for as long as possible — the model is the thing
 * being looked at. The scene panel narrows first, then moves to a row.
 *
 * NOTE: two elements assigned the same grid-area OVERLAP, they do not stack.
 * Every step below gives the scene panel its own named area rather than reusing
 * "panel", which is the mistake this arrangement replaced.
 */
@media (max-width: 1180px) {
	:root { --results-width: 244px; }
}

@media (max-width: 980px) {
	.app {
		grid-template-columns: var(--panel-width) minmax(0, 1fr);
		grid-template-rows: auto minmax(0, 1fr) auto auto auto;
		grid-template-areas:
			"top     top"
			"panel   view"
			"scene   scene"
			"status  status"
			"log     log";
	}
	.scene { max-height: 240px; border-left: none; border-top: 1px solid var(--line-soft); }
}

@media (max-width: 860px) {
	.app {
		grid-template-columns: 1fr;
		grid-template-rows: auto minmax(240px, 42dvh) auto auto auto auto;
		grid-template-areas:
			"top"
			"view"
			"panel"
			"scene"
			"status"
			"log";
	}
	.panel { border-right: none; border-top: 1px solid var(--line-soft); }
	.viewport-hint { display: none; }
	.drawer { height: 150px; }
}
