Download style.css from constructelligence/data-centre-nameplates: direct link, hf CLI and curl.
- Browser
- Download file 64.2 kB
-
https://huggingface.co/spaces/constructelligence/data-centre-nameplates/resolve/main/style.css
- Command line
-
hf download hf://spaces/constructelligence/data-centre-nameplates/style.css
-
curl -L -o style.css https://huggingface.co/spaces/constructelligence/data-centre-nameplates/resolve/main/style.css
64.2 kB
| :root { | |
| /* Primary white / secondary dark navy — a Procore/Autodesk-style light | |
| professional chrome instead of a dark "hacker tool" theme. Navy does | |
| double duty as both structural accent (active tabs, focus rings) and | |
| the default detection-box color, so the whole app reads as one brand | |
| rather than chrome + an unrelated accent hue. */ | |
| --bg: #ffffff; | |
| --bg-panel: #ffffff; | |
| --bg-panel-2: #f3f5f9; | |
| --bg-subtle: #eef1f6; | |
| --border: #dde3ec; | |
| --border-strong: #0f2547; | |
| --text: #101d30; | |
| --text-dim: #5c6b80; | |
| --accent: #0f2547; | |
| --accent-hover: #18345c; | |
| --accent-soft: #e8edf5; | |
| --success: #16a34a; | |
| --success-soft: #e6f7ec; | |
| --danger: #dc2626; | |
| --danger-soft: #fdecec; | |
| --warning: #b45309; | |
| /* Small and mostly-uniform on purpose — flat/square, not the pill/rounded | |
| treatment of a generic AI-tool template. */ | |
| --radius: 6px; | |
| --radius-sm: 4px; | |
| --radius-lg: 8px; | |
| --shadow-xs: 0 1px 2px rgba(15, 37, 71, 0.06); | |
| --shadow-sm: 0 1px 3px rgba(15, 37, 71, 0.08); | |
| --shadow-md: 0 4px 16px rgba(15, 37, 71, 0.10); | |
| font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | |
| } | |
| * { box-sizing: border-box; } | |
| html, body { | |
| margin: 0; | |
| padding: 0; | |
| background: var(--bg); | |
| color: var(--text); | |
| height: 100%; | |
| } | |
| body { | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| button, input, select { font-family: inherit; } | |
| /* ---------- Header ---------- */ | |
| .app-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 16px; | |
| padding: 12px 20px; | |
| border-bottom: 1px solid var(--border); | |
| background: var(--bg-panel); | |
| flex: 0 0 auto; | |
| } | |
| .brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-weight: 700; | |
| letter-spacing: -0.01em; | |
| } | |
| .brand-mark { | |
| width: 22px; | |
| height: 22px; | |
| flex-shrink: 0; | |
| color: var(--accent); | |
| } | |
| .brand-name { | |
| font: inherit; | |
| font-size: 1.05rem; | |
| font-weight: inherit; | |
| letter-spacing: inherit; | |
| margin: 0; | |
| color: var(--accent); | |
| } | |
| .mode-switch { | |
| display: flex; | |
| gap: 2px; | |
| background: var(--bg-subtle); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 3px; | |
| min-width: 0; | |
| overflow-x: auto; | |
| scrollbar-width: none; | |
| -webkit-overflow-scrolling: touch; | |
| /* Edge fade as a scroll affordance, but only on the side that actually has | |
| more to reach: the old rule faded both edges unconditionally, so a bar | |
| scrolled hard to the left still dimmed its leftmost tab as though | |
| something were hidden behind it. `.at-start` / `.at-end` are set from | |
| the scroll handler in main.js. With 9 modes in a 348px-wide bar, over | |
| half the tabs are off-screen on a phone — this is the only thing telling | |
| you Measure, Landmarks, Sketch and Drone exist at all. */ | |
| --fade-l: 14px; | |
| --fade-r: 14px; | |
| -webkit-mask-image: linear-gradient(to right, transparent, black var(--fade-l), black calc(100% - var(--fade-r)), transparent); | |
| mask-image: linear-gradient(to right, transparent, black var(--fade-l), black calc(100% - var(--fade-r)), transparent); | |
| } | |
| .mode-switch.at-start { --fade-l: 0px; } | |
| .mode-switch.at-end { --fade-r: 0px; } | |
| .mode-switch::-webkit-scrollbar { display: none; } | |
| .mode-btn { | |
| flex: 0 0 auto; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| border: none; | |
| background: transparent; | |
| color: var(--text-dim); | |
| padding: 7px 12px; | |
| border-radius: var(--radius-sm); | |
| font-size: 0.82rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| white-space: nowrap; | |
| transition: background 0.12s, color 0.12s; | |
| } | |
| .mode-btn:hover { color: var(--text); background: rgba(15, 37, 71, 0.06); } | |
| .mode-btn.active { | |
| background: var(--accent); | |
| color: #ffffff; | |
| } | |
| /* Tabs that navigate to another page (Circuits) — look like .mode-btn without main.js binding them. */ | |
| .mode-link { | |
| flex: 0 0 auto; display: flex; align-items: center; gap: 6px; padding: 7px 12px; | |
| border-radius: var(--radius-sm); font-size: 0.82rem; font-weight: 600; white-space: nowrap; | |
| color: var(--text-dim); text-decoration: none; transition: background 0.12s, color 0.12s; | |
| } | |
| a.mode-link:hover { color: var(--text); background: rgba(15, 37, 71, 0.06); } | |
| .mode-link.active { background: var(--accent); color: #ffffff; } | |
| /* Icon sprites — sized down from their 24x24 viewBox for compact chrome use. | |
| currentColor means an icon always matches its button's current text color | |
| (dim/hover/active states) with no extra rules needed. */ | |
| .mode-icon { | |
| width: 14px; | |
| height: 14px; | |
| flex-shrink: 0; | |
| color: currentColor; | |
| } | |
| .title-icon { | |
| width: 15px; | |
| height: 15px; | |
| flex-shrink: 0; | |
| margin-right: 6px; | |
| vertical-align: -2px; | |
| color: var(--accent); | |
| } | |
| .header-status { display: flex; align-items: center; gap: 8px; } | |
| .status-pill { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-size: 0.75rem; | |
| color: var(--text-dim); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| padding: 4px 10px; | |
| white-space: nowrap; | |
| background: var(--bg-panel-2); | |
| } | |
| .status-pill.ready { color: var(--success); border-color: var(--success-soft); background: var(--success-soft); } | |
| .status-pill.loading { color: var(--accent); border-color: var(--accent-soft); background: var(--accent-soft); } | |
| .model-status { font-family: inherit; cursor: default; } | |
| .model-status[aria-busy="true"] { cursor: progress; } | |
| .model-status.error { color: #982b2b; border-color: #edcaca; background: #fff2f2; cursor: pointer; } | |
| .model-status.error:focus-visible { outline: 2px solid #982b2b; outline-offset: 2px; } | |
| .weather-pill { | |
| font-family: inherit; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: border-color 0.12s, background 0.12s; | |
| } | |
| .weather-pill:hover { border-color: var(--accent); background: var(--accent-soft); } | |
| .weather-pill.loaded { color: var(--accent); border-color: var(--accent-soft); background: var(--accent-soft); } | |
| /* ---------- Layout ---------- */ | |
| .layout { | |
| flex: 1 1 auto; | |
| display: grid; | |
| grid-template-columns: 1fr 320px; | |
| gap: 16px; | |
| padding: 16px; | |
| min-height: 0; | |
| background: var(--bg-subtle); | |
| } | |
| .stage { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| min-width: 0; | |
| } | |
| .stage-frame { | |
| position: relative; | |
| flex: 1 1 auto; | |
| /* The video/photo stage stays dark regardless of the app's light chrome — | |
| footage needs a neutral black surround the same way a photo viewer or | |
| video editor in a light-themed pro tool still letterboxes on black. */ | |
| background: #05070a; | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| overflow: hidden; | |
| min-height: 420px; | |
| box-shadow: var(--shadow-xs); | |
| background-image: | |
| linear-gradient(rgba(120, 155, 200, 0.05) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(120, 155, 200, 0.05) 1px, transparent 1px); | |
| background-size: 24px 24px; | |
| } | |
| .scan-sweep { display:none; position:absolute; z-index:5; top:0; left:0; right:0; height:2px; pointer-events:none; opacity:0; background:linear-gradient(90deg, transparent 0%, rgba(56,189,248,.18) 12%, #67e8f9 50%, rgba(56,189,248,.18) 88%, transparent 100%); box-shadow:0 0 12px rgba(34,211,238,.62); } | |
| .stage-frame.is-scanning .scan-sweep { display:block; animation:scan-sweep 1.35s ease-in-out infinite; } | |
| @keyframes scan-sweep { 0% { transform:translateY(0); opacity:0; } 10% { opacity:.72; } 88% { opacity:.72; } 100% { transform:translateY(var(--scan-distance, 100vh)); opacity:0; } } | |
| .scan-status { position:absolute; z-index:7; top:12px; right:12px; display:flex; align-items:center; gap:8px; padding:7px 10px; border:1px solid rgba(203,213,225,.22); border-radius:5px; background:rgba(8,18,32,.88); color:#f2f6fb; font-size:10px; line-height:1; font-weight:650; letter-spacing:.035em; text-transform:uppercase; box-shadow:0 4px 14px rgba(0,0,0,.2); animation:scan-status-in .16s ease-out both; pointer-events:none; } | |
| .scan-status[hidden] { display:none; } | |
| .scan-status-dot { width:6px; height:6px; border-radius:50%; background:#60a5fa; box-shadow:0 0 0 0 rgba(96,165,250,.48); animation:scan-dot 1.1s ease-out infinite; } | |
| @keyframes scan-status-in { from { opacity:0; transform:translateY(-4px); } to { opacity:1; transform:translateY(0); } } | |
| @keyframes scan-dot { 65% { box-shadow:0 0 0 5px rgba(103,232,249,0); } 100% { box-shadow:0 0 0 0 rgba(103,232,249,0); } } | |
| #video, #overlay { | |
| position: absolute; | |
| inset: 0; | |
| width: 100%; | |
| height: 100%; | |
| object-fit: contain; | |
| } | |
| #video { background: #000; } | |
| #overlay { pointer-events: none; } | |
| /* Persistent mode context, anchored to the stage itself rather than only the | |
| sidebar — the same "always know what you're looking at" pattern a | |
| Procore/Autodesk canvas keeps visible regardless of scroll position or, | |
| on mobile, whether the settings sheet is even open. */ | |
| /* Bottom-left, not top-left. Detection labels are drawn above their boxes | |
| and clamp to the top edge when a box runs off frame, so the top-left | |
| corner is exactly where overlay labels pile up — the badge was landing on | |
| top of "Building 83%" on a normal skyline shot. Nothing is ever drawn | |
| below the bottom-most box, so this corner stays clear. */ | |
| .stage-caption { | |
| position: absolute; | |
| bottom: 10px; | |
| left: 10px; | |
| z-index: 6; | |
| display: flex; | |
| align-items: center; | |
| gap: 7px; | |
| padding: 6px 9px; | |
| border: 1px solid rgba(203, 213, 225, 0.2); | |
| background: rgba(8, 18, 32, 0.84); | |
| color: #ffffff; | |
| border-radius: 5px; | |
| font-size: 0.68rem; | |
| font-weight: 650; | |
| letter-spacing: 0.055em; | |
| text-transform: uppercase; | |
| box-shadow: 0 3px 12px rgba(0,0,0,.16); | |
| pointer-events: none; | |
| } | |
| .stage-caption .mode-icon { color: #ffffff; } | |
| /* With no source loaded there's nothing on the stage to caption, and the | |
| badge landed directly on top of the empty state's heading on a narrow | |
| screen. The active mode tab already says which mode you're in, so the | |
| badge has no job here until there's a frame under it. */ | |
| .stage-frame:not(.has-source) .stage-caption { display: none; } | |
| /* Centered when it fits, scrollable when it doesn't. The old fixed | |
| `justify-content: center` silently pushed content past the frame's clipped | |
| edges on a short stage — on a 390x844 phone the "Upload Photo/Video" | |
| button landed 45px below the frame's bottom and was simply unreachable, | |
| which is a dead end on the device this app is most used from. `margin: | |
| auto` on the inner block centers exactly like the old rule while still | |
| letting overflow scroll into reach, so this holds at any viewport rather | |
| than at the sizes that happened to be checked. */ | |
| .empty-state { | |
| position: absolute; | |
| inset: 0; | |
| display: flex; | |
| flex-direction: column; | |
| text-align: center; | |
| padding: 40px; | |
| overflow-y: auto; | |
| overscroll-behavior: contain; | |
| color: #e8edf3; | |
| } | |
| .empty-state-inner { | |
| margin: auto; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 12px; | |
| width: 100%; | |
| } | |
| /* Line-art mark rather than the 🏙️ emoji this used to be: emoji render at | |
| the mercy of the platform's own font (a flat blue-grey block on this | |
| machine at 2.4rem, out of key with every other icon in the app), and the | |
| header/panel icons are already an SVG sprite set — this now matches them. */ | |
| .empty-icon { | |
| color: #6d86a6; | |
| display: flex; | |
| } | |
| .empty-hint { | |
| font-size: 0.78rem; | |
| color: #6d7c90; | |
| margin: 2px 0 0; | |
| } | |
| .empty-state h2 { | |
| font-size: 1.3rem; | |
| font-weight: 700; | |
| margin: 0; | |
| max-width: 26ch; | |
| } | |
| .empty-state p { | |
| color: #98a5b8; | |
| max-width: 42ch; | |
| margin: 0; | |
| font-size: 0.9rem; | |
| line-height: 1.5; | |
| } | |
| .empty-actions { | |
| display: flex; | |
| gap: 10px; | |
| margin-top: 8px; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| } | |
| .stage-frame.drag-active { | |
| outline: 2px dashed var(--accent); | |
| outline-offset: -2px; | |
| } | |
| .loading-overlay[hidden] { display: none; } | |
| .loading-overlay { | |
| position: absolute; | |
| inset: 0; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 12px; | |
| background: rgba(5, 7, 10, 0.91); | |
| font-size: 0.9rem; | |
| color: #b6c0cf; | |
| letter-spacing: .025em; | |
| backdrop-filter: blur(2px); | |
| } | |
| .spinner { | |
| width: 28px; | |
| height: 28px; | |
| border-radius: 50%; | |
| border: 3px solid #232b36; | |
| border-top-color: var(--accent-hover); | |
| animation: spin 0.8s linear infinite; | |
| } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| .stage-toolbar { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .detection-count { | |
| margin-left: auto; | |
| font-size: 0.72rem; | |
| font-weight: 600; | |
| letter-spacing: .015em; | |
| color: var(--text-dim); | |
| } | |
| .detection-count.readout-change { animation:readout-change .24s ease-out; } | |
| @keyframes readout-change { from { opacity:.45; transform:translateY(2px); } to { opacity:1; transform:translateY(0); } } | |
| /* ---------- Buttons ---------- */ | |
| .btn { | |
| border: 1px solid var(--border); | |
| background: var(--bg-panel); | |
| color: var(--text); | |
| padding: 9px 16px; | |
| border-radius: var(--radius-sm); | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: border-color 0.12s, background 0.12s, box-shadow 0.12s; | |
| } | |
| .btn:hover { border-color: var(--accent); background: var(--accent-soft); } | |
| .btn-primary { | |
| background: var(--accent); | |
| border-color: var(--accent); | |
| color: #ffffff; | |
| } | |
| .btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); } | |
| .btn-secondary { | |
| background: var(--bg-panel-2); | |
| } | |
| .btn-secondary:hover { background: var(--accent-soft); } | |
| .btn-ghost { | |
| background: transparent; | |
| border-color: var(--border); | |
| padding: 7px 14px; | |
| } | |
| /* Buttons and mode tabs had no focus style at all — the browser default is | |
| suppressed by the custom backgrounds, so keyboard and switch-control users | |
| had no way to tell what was focused anywhere in the app's primary | |
| controls. `:focus-visible` rather than `:focus` so a mouse click doesn't | |
| leave a ring behind. Offset outward so it reads clearly against both the | |
| filled primary button and the tinted tab bar. */ | |
| .btn:focus-visible, | |
| .mode-btn:focus-visible, | |
| .file-btn:focus-within { | |
| outline: 2px solid var(--accent); | |
| outline-offset: 2px; | |
| } | |
| /* The active tab is already navy-filled, so a navy ring on it would be | |
| invisible — it gets a light ring against its own dark background instead. */ | |
| .mode-btn.active:focus-visible, | |
| .btn-primary:focus-visible { | |
| outline-color: #ffffff; | |
| box-shadow: 0 0 0 4px rgba(15, 37, 71, 0.45); | |
| } | |
| .file-btn { display: inline-flex; align-items: center; } | |
| .btn-full { width: 100%; } | |
| /* ---------- Sidebar ---------- */ | |
| .sidebar { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| overflow-y: auto; | |
| } | |
| .panel { | |
| background: var(--bg-panel); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 14px; | |
| transition: box-shadow 0.15s, border-color 0.15s; | |
| } | |
| .panel:hover { box-shadow: var(--shadow-xs); } | |
| .panel-title { | |
| display: flex; | |
| align-items: center; | |
| font-size: 0.8rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.04em; | |
| color: var(--accent); | |
| margin: 0 0 8px; | |
| font-weight: 700; | |
| } | |
| /* ---------- Stat tiles (Job Site / BIM) ---------- */ | |
| /* A live KPI row — the same "number, then its label, in a small card" pattern | |
| a PowerBI/Procore dashboard leads with, instead of burying these same | |
| figures only in canvas-drawn overlay text or a plain sentence. */ | |
| .stat-tiles { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 6px; | |
| margin: 0 0 14px; | |
| } | |
| .stat-tile { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2px; | |
| padding: 8px 6px; | |
| background: var(--bg-panel-2); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| text-align: center; | |
| } | |
| .stat-value { | |
| font-size: 1.05rem; | |
| font-weight: 700; | |
| color: var(--accent); | |
| line-height: 1.15; | |
| font-variant-numeric: tabular-nums; | |
| } | |
| .stat-label { | |
| font-size: 0.62rem; | |
| color: var(--text-dim); | |
| text-transform: uppercase; | |
| letter-spacing: 0.03em; | |
| line-height: 1.3; | |
| } | |
| /* A tile left alone on the last row of the 3-up grid spans it instead of | |
| sitting in a third of the width with two empty cells beside it. That's | |
| BIM's "Completion stage", the one tile here whose value is a phrase rather | |
| than a number — "Envelope in progress" was wrapping onto three cramped | |
| lines in a narrow cell. Full width also lets it lay out as a row (label | |
| left, value right), which is the right shape for a text value: the other | |
| tiles are scanned as a row of numbers, this one is read as a sentence. | |
| Written as "last child that starts a row" so it applies wherever that | |
| shape occurs rather than being pinned to the BIM panel specifically. */ | |
| .stat-tiles > .stat-tile:last-child:nth-child(3n + 1) { | |
| grid-column: 1 / -1; | |
| flex-direction: row-reverse; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 10px; | |
| text-align: left; | |
| padding: 9px 11px; | |
| } | |
| .stat-tiles > .stat-tile:last-child:nth-child(3n + 1) .stat-value { | |
| font-size: 0.9rem; | |
| text-align: right; | |
| } | |
| .stat-tiles > .stat-tile:last-child:nth-child(3n + 1) .stat-label { | |
| flex: 0 0 auto; | |
| } | |
| /* Four figures of the same kind (Drone): a 2x2 block, not three plus a | |
| stretched fourth — the span-the-row rule above is for a lone text value. */ | |
| .stat-tiles.quad { grid-template-columns: repeat(2, 1fr); } | |
| .stat-tiles.quad > .stat-tile:last-child:nth-child(3n + 1) { | |
| grid-column: auto; | |
| flex-direction: column; | |
| text-align: center; | |
| gap: 2px; | |
| padding: 8px 6px; | |
| } | |
| .stat-tiles.quad > .stat-tile:last-child:nth-child(3n + 1) .stat-value { font-size: 1.05rem; text-align: center; } | |
| /* Drone land-cover breakdown: one stacked bar (the whole frame) + legend. */ | |
| .land-cover { margin: 0 0 14px; } | |
| .land-cover-bar { | |
| display: flex; | |
| height: 12px; | |
| border-radius: var(--radius-sm); | |
| overflow: hidden; | |
| background: var(--bg-subtle); | |
| border: 1px solid var(--border); | |
| } | |
| .land-cover-bar > span { flex-basis: 0; min-width: 2px; } | |
| .land-cover-bar > span + span { box-shadow: -1px 0 0 var(--bg-panel); } | |
| .land-cover-legend { | |
| list-style: none; | |
| margin: 8px 0 0; | |
| padding: 0; | |
| display: grid; | |
| gap: 3px; | |
| font-size: 0.74rem; | |
| } | |
| .land-cover-legend li { display: flex; align-items: center; gap: 7px; min-width: 0; } | |
| .land-cover-legend i { flex: 0 0 10px; height: 10px; border-radius: 2px; } | |
| .land-cover-legend span { flex: 1 1 auto; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | |
| .land-cover-legend b { font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; } | |
| .drone-scale { display: grid; gap: 8px; margin: 0 0 12px; border: 0; padding: 0; min-width: 0; } | |
| .drone-scale legend { font-size: 0.82rem; font-weight: 600; color: var(--text); padding: 0 0 6px; } | |
| .drone-scale label { display: grid; gap: 4px; font-size: 0.78rem; color: var(--text-dim); } | |
| .drone-scale .panel-hint { margin: 0; } | |
| /* ---------- Toggle groups ---------- */ | |
| /* A tinted card around a set of related checkboxes — the same "grouped | |
| settings" convention Procore/Autodesk panels use so a cluster of options | |
| reads as one decision ("which element types") rather than a flat list of | |
| unrelated checkboxes. */ | |
| .toggle-group { | |
| background: var(--bg-panel-2); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| padding: 10px 12px; | |
| } | |
| .panel-hint { | |
| font-size: 0.78rem; | |
| line-height: 1.5; | |
| color: var(--text-dim); | |
| margin: 0 0 12px; | |
| } | |
| .field { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 6px; | |
| font-size: 0.82rem; | |
| margin-bottom: 10px; | |
| } | |
| .field:last-child { margin-bottom: 0; } | |
| .field.checkbox { | |
| flex-direction: row; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .field input[type="range"] { accent-color: var(--accent); } | |
| .field input[type="color"] { | |
| width: 100%; | |
| height: 32px; | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| background: none; | |
| padding: 2px; | |
| } | |
| .class-filter { | |
| border: none; | |
| padding: 0; | |
| margin: 0 0 10px; | |
| } | |
| .class-filter legend { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| width: 100%; | |
| font-size: 0.82rem; | |
| padding: 0 0 6px; | |
| color: var(--text); | |
| font-weight: 600; | |
| } | |
| .filter-actions { display:inline-flex; gap:4px; margin-left:auto; } | |
| .filter-actions button { min-height:28px; padding:3px 8px; border:1px solid var(--border); border-radius:4px; background:var(--bg-panel); color:var(--text-dim); font-family:inherit; font-size:11px; font-weight:600; line-height:1; cursor:pointer; } | |
| .filter-actions button:hover { color:var(--accent); border-color:var(--accent); background:var(--accent-soft); } | |
| .filter-actions button:focus-visible { outline:2px solid var(--accent); outline-offset:2px; } | |
| .class-filter label { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 0.82rem; | |
| padding: 4px 0; | |
| color: var(--text); | |
| } | |
| .about-panel { color: var(--text-dim); } | |
| /* Disclosure wrapper applied by collapseLongHints() in main.js. Styled as a | |
| quiet inline affordance, not a button — it's supporting detail you can open, | |
| and it shouldn't compete with the panel's actual controls for attention. */ | |
| .hint-details { | |
| margin: 6px 0 0; | |
| } | |
| .hint-details > summary { | |
| cursor: pointer; | |
| list-style: none; | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| color: var(--text-dim); | |
| padding: 5px 0; | |
| user-select: none; | |
| } | |
| .hint-details > summary::-webkit-details-marker { display: none; } | |
| /* Own triangle, so it can rotate on open and sit at the size/color the rest | |
| of the sidebar uses rather than the browser's default marker. */ | |
| .hint-details > summary::before { | |
| content: ""; | |
| width: 0; | |
| height: 0; | |
| border-left: 4px solid currentColor; | |
| border-top: 3.5px solid transparent; | |
| border-bottom: 3.5px solid transparent; | |
| transition: transform 0.15s ease; | |
| } | |
| .hint-details[open] > summary::before { transform: rotate(90deg) translateX(-1px); } | |
| .hint-details > summary:hover { color: var(--accent); } | |
| .hint-details > summary:focus-visible { | |
| outline: 2px solid var(--accent); | |
| outline-offset: 2px; | |
| border-radius: var(--radius-sm); | |
| } | |
| .hint-details .panel-hint.small { | |
| margin-top: 0; | |
| padding-left: 9px; | |
| border-left: 2px solid var(--border); | |
| } | |
| .panel-hint.small { | |
| font-size: 0.72rem; | |
| margin-top: -4px; | |
| } | |
| .landmark-permissions { | |
| display: flex; | |
| gap: 8px; | |
| flex-wrap: wrap; | |
| margin-bottom: 10px; | |
| } | |
| .landmark-permissions .btn { | |
| flex: 1 1 auto; | |
| padding: 8px 10px; | |
| font-size: 0.78rem; | |
| } | |
| .status-text { | |
| font-size: 0.76rem; | |
| color: var(--text-dim); | |
| margin: 0 0 4px; | |
| } | |
| .status-text.active { color: var(--success); } | |
| .landmark-list { | |
| list-style: none; | |
| margin: 10px 0 0; | |
| padding: 0; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 6px; | |
| max-height: 260px; | |
| overflow-y: auto; | |
| } | |
| .landmark-list li { | |
| display: flex; | |
| justify-content: space-between; | |
| gap: 10px; | |
| font-size: 0.8rem; | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| padding: 7px 10px; | |
| background: var(--bg-panel-2); | |
| } | |
| .landmark-list .lm-name { | |
| font-weight: 600; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .zone-remove-btn { | |
| flex: 0 0 auto; | |
| padding: 3px 9px; | |
| font-size: 0.72rem; | |
| } | |
| .lm-height { | |
| color: var(--text-dim); | |
| font-weight: 400; | |
| font-size: 0.75rem; | |
| } | |
| .landmark-list .lm-meta { | |
| color: var(--text-dim); | |
| white-space: nowrap; | |
| flex: 0 0 auto; | |
| } | |
| .safety-entry-copy { display:flex; min-width:0; flex-direction:column; gap:3px; } | |
| .safety-entry-detail { color:var(--text-dim); font-size:11px; line-height:1.4; overflow-wrap:anywhere; } | |
| .landmark-list li.empty { | |
| color: var(--text-dim); | |
| justify-content: center; | |
| font-style: italic; | |
| } | |
| #occupancySparkline { | |
| width: 100%; | |
| height: 40px; | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| background: var(--bg-panel-2); | |
| display: block; | |
| } | |
| #overlay.canvas-pointer-active { | |
| pointer-events: auto; | |
| cursor: crosshair; | |
| } | |
| .legend-swatch { | |
| display: inline-block; | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 2px; | |
| margin-right: 6px; | |
| flex: 0 0 auto; | |
| } | |
| .reference-input-row[hidden] { display: none; } | |
| .reference-input-row { | |
| display: flex; | |
| gap: 6px; | |
| margin-top: 8px; | |
| } | |
| .reference-input-row input { | |
| flex: 1 1 auto; | |
| min-width: 0; | |
| background: var(--bg-panel-2); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| color: var(--text); | |
| padding: 7px 8px; | |
| font-size: 0.82rem; | |
| } | |
| .reference-input-row select { | |
| background: var(--bg-panel-2); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| color: var(--text); | |
| font-size: 0.82rem; | |
| padding: 7px 4px; | |
| } | |
| #tradeCategorySelect, #referenceUnitSelect { | |
| background: var(--bg-panel-2); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| color: var(--text); | |
| padding: 8px; | |
| font-size: 0.85rem; | |
| } | |
| .text-input-flex { | |
| flex: 1 1 auto; | |
| min-width: 0; | |
| background: var(--bg-panel-2); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| color: var(--text); | |
| padding: 8px 10px; | |
| font-size: 0.82rem; | |
| } | |
| #tradeNoteInput { | |
| background: var(--bg-panel-2); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| color: var(--text); | |
| padding: 8px 10px; | |
| font-size: 0.82rem; | |
| width: 100%; | |
| } | |
| input[type="text"]:focus, input[type="number"]:focus, select:focus, .text-input-flex:focus { | |
| outline: none; | |
| border-color: var(--accent); | |
| box-shadow: 0 0 0 3px var(--accent-soft); | |
| } | |
| /* ---------- Toggles (checkboxes) ---------- */ | |
| /* Flat, square, native <input type=checkbox> restyled via appearance:none — | |
| no rounded pill/circle anywhere, a filled navy square with a white | |
| checkmark when checked. Applies globally so every existing checkbox | |
| (class filters, safety flags, display panel, etc.) picks this up without | |
| touching markup. */ | |
| input[type="checkbox"] { | |
| appearance: none; | |
| -webkit-appearance: none; | |
| width: 16px; | |
| height: 16px; | |
| flex: 0 0 auto; | |
| border: 1.5px solid var(--border-strong); | |
| border-radius: 3px; | |
| background: var(--bg-panel); | |
| cursor: pointer; | |
| position: relative; | |
| margin: 0; | |
| transition: background-color 0.12s, border-color 0.12s; | |
| } | |
| input[type="checkbox"]:hover { border-color: var(--accent); } | |
| input[type="checkbox"]:checked { | |
| background: var(--accent); | |
| border-color: var(--accent); | |
| } | |
| input[type="checkbox"]:checked::after { | |
| content: ''; | |
| position: absolute; | |
| left: 4px; | |
| top: 1px; | |
| width: 4px; | |
| height: 8px; | |
| border: solid #ffffff; | |
| border-width: 0 2px 2px 0; | |
| transform: rotate(45deg); | |
| } | |
| input[type="checkbox"]:focus-visible { | |
| outline: 2px solid var(--accent); | |
| outline-offset: 2px; | |
| } | |
| /* ---------- Responsive ---------- */ | |
| @media (max-width: 860px) { | |
| .layout { | |
| display: flex; | |
| flex-direction: column; | |
| overflow-y: auto; | |
| } | |
| .stage { flex: 0 0 auto; } | |
| .stage-frame { flex: 0 0 auto; } | |
| .sidebar { flex: 0 0 auto; overflow: visible; } | |
| .app-header { | |
| flex-wrap: wrap; | |
| } | |
| .stage-frame { | |
| min-height: 280px; | |
| } | |
| /* iOS Safari auto-zooms the page on focusing any input under 16px — these | |
| are the ones a user is likely to tap on a phone (text/number fields, | |
| dropdowns), so they get bumped regardless of their desktop size. */ | |
| input[type="text"], | |
| input[type="number"], | |
| select, | |
| .text-input-flex { | |
| font-size: 16px; | |
| } | |
| .btn, | |
| .mode-btn { | |
| min-height: 40px; | |
| } | |
| .empty-actions .btn, | |
| .btn-full { | |
| min-height: 44px; | |
| } | |
| /* The detection-count text (BIM/Electrical stats can run long) was liable | |
| to get squeezed against the toolbar buttons on a narrow screen — letting | |
| it wrap to its own full-width row keeps both legible. */ | |
| .stage-toolbar { | |
| flex-wrap: wrap; | |
| } | |
| .detection-count { | |
| margin-left: 0; | |
| flex: 1 1 100%; | |
| text-align: right; | |
| } | |
| /* Native range/checkbox controls have a small hit area by default — this | |
| grows it without changing how big they look, easier to hit precisely | |
| with a finger instead of a mouse pointer. */ | |
| input[type="range"] { | |
| height: 28px; | |
| } | |
| input[type="checkbox"] { | |
| width: 20px; | |
| height: 20px; | |
| } | |
| input[type="checkbox"]:checked::after { | |
| left: 6px; | |
| top: 2px; | |
| width: 5px; | |
| height: 10px; | |
| } | |
| /* The checkbox graphic grew to 20px above, but the thing a finger actually | |
| aims at is the whole label row, and that was still only 20px tall — well | |
| under the ~44px every touch guideline asks for, and this app is operated | |
| one-handed, outdoors, often in work gloves. Padding the row (rather than | |
| inflating the box) keeps the visual density while making the real hit | |
| area a comfortable target. */ | |
| .field.checkbox, | |
| .class-filter label, | |
| .hint-details > summary { | |
| min-height: 44px; | |
| padding: 4px 0; | |
| align-items: center; | |
| } | |
| /* The stage is the whole point of the app on a phone, and 280px of a | |
| 844px screen left it smaller than the settings text below it. Sized | |
| against the viewport so it scales with the device instead of being a | |
| fixed number tuned for one handset. */ | |
| .stage-frame { | |
| min-height: min(56vh, 460px); | |
| } | |
| /* Three stacked header rows (brand / tabs / status) ate 168px before the | |
| stage even started. The brand and the status pills have room to share a | |
| row at any phone width, which gives that space back to the stage. */ | |
| .app-header { | |
| gap: 8px; | |
| padding: 10px 14px; | |
| } | |
| .brand { flex: 1 1 auto; } | |
| .header-status { order: 2; } | |
| .mode-switch { order: 3; flex: 1 1 100%; } | |
| .empty-state { | |
| padding: 20px 16px; | |
| } | |
| .empty-state h2 { | |
| font-size: 1.15rem; | |
| } | |
| /* Both actions sit on one row rather than stacking — "Upload Photo/Video" | |
| is the primary path for anyone reviewing footage shot earlier, and | |
| burying it below the fold is what made it unreachable before. */ | |
| .empty-actions { | |
| width: 100%; | |
| flex-wrap: nowrap; | |
| } | |
| .empty-actions .btn { | |
| flex: 1 1 0; | |
| padding: 9px 8px; | |
| justify-content: center; | |
| text-align: center; | |
| } | |
| } | |
| /* ---------- Reduced motion ---------- */ | |
| /* The spinner, the tab-bar's smooth scroll, and every hover/open transition | |
| are decoration; a user who has asked their OS for less motion should not | |
| have to sit through them. The spinner keeps a slow rotation rather than | |
| freezing outright — a completely static "loading" indicator reads as a | |
| hung app, which is worse than no motion at all. */ | |
| @media (prefers-reduced-motion: reduce) { | |
| *, *::before, *::after { | |
| animation-duration: 0.01ms ; | |
| animation-iteration-count: 1 ; | |
| transition-duration: 0.01ms ; | |
| scroll-behavior: auto ; | |
| } | |
| .spinner { | |
| animation-duration: 1.6s ; | |
| animation-iteration-count: infinite ; | |
| } | |
| } | |
| /* ============================================================ | |
| Landing page | |
| ============================================================ | |
| The app's front door. It shares the app's tokens (navy accent, flat | |
| 6px radii, Inter) so the marketing surface and the tool read as one | |
| product rather than a template bolted onto a tool. Nothing here is | |
| loaded or measured by the app itself — when the shell is revealed the | |
| whole landing element is simply removed from the flow. */ | |
| .landing { | |
| --lp-max: 1120px; | |
| background: var(--bg); | |
| color: var(--text); | |
| overflow-x: hidden; | |
| } | |
| .landing[hidden] { display: none; } | |
| /* The app shell fills the viewport via flex on <body>; the landing is a | |
| normal scrolling document, so <body> has to stop stretching it. */ | |
| body:has(.landing:not([hidden])) { display: block; height: auto; } | |
| .app-shell { | |
| display: flex; | |
| flex-direction: column; | |
| flex: 1 1 auto; | |
| min-height: 0; | |
| } | |
| .app-shell[hidden] { display: none; } | |
| /* ---------- Landing nav ---------- */ | |
| .lp-nav { | |
| position: sticky; | |
| top: 0; | |
| z-index: 20; | |
| display: flex; | |
| align-items: center; | |
| gap: 20px; | |
| max-width: var(--lp-max); | |
| margin: 0 auto; | |
| padding: 14px 24px; | |
| background: rgba(255, 255, 255, 0.88); | |
| backdrop-filter: saturate(1.6) blur(10px); | |
| } | |
| .lp-brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-right: auto; | |
| text-decoration: none; | |
| color: var(--accent); | |
| font-weight: 700; | |
| letter-spacing: -0.01em; | |
| } | |
| .lp-brand .brand-name { font-size: 1.05rem; } | |
| .lp-nav-links { display: flex; gap: 22px; } | |
| .lp-nav-links a { | |
| color: var(--text-dim); | |
| text-decoration: none; | |
| font-size: 0.88rem; | |
| font-weight: 600; | |
| } | |
| .lp-nav-links a:hover { color: var(--accent); } | |
| .lp-nav-links a:focus-visible, | |
| .lp-brand:focus-visible { | |
| outline: 2px solid var(--accent); | |
| outline-offset: 3px; | |
| border-radius: var(--radius-sm); | |
| } | |
| /* ---------- Hero ---------- */ | |
| .lp-hero { | |
| max-width: var(--lp-max); | |
| margin: 0 auto; | |
| padding: 56px 24px 0; | |
| text-align: center; | |
| } | |
| .lp-eyebrow { | |
| margin: 0 0 18px; | |
| font-size: 0.8rem; | |
| font-weight: 700; | |
| letter-spacing: 0.06em; | |
| text-transform: uppercase; | |
| color: var(--text-dim); | |
| } | |
| /* clamp() rather than breakpoints: the headline is the one element that | |
| has to hold its line-shape from a 390px phone to a wide desktop. */ | |
| .lp-h1 { | |
| margin: 0 auto 20px; | |
| max-width: 18ch; | |
| font-size: clamp(2.1rem, 5.4vw, 3.7rem); | |
| line-height: 1.05; | |
| letter-spacing: -0.03em; | |
| font-weight: 800; | |
| color: var(--accent); | |
| } | |
| .lp-lede { | |
| margin: 0 auto; | |
| max-width: 62ch; | |
| font-size: clamp(1rem, 1.5vw, 1.15rem); | |
| line-height: 1.6; | |
| color: var(--text-dim); | |
| } | |
| .lp-cta-row { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 12px; | |
| justify-content: center; | |
| margin: 28px 0 12px; | |
| } | |
| .btn-lg { | |
| padding: 13px 26px; | |
| font-size: 0.95rem; | |
| font-weight: 700; | |
| text-decoration: none; | |
| min-height: 48px; | |
| align-items: center; | |
| } | |
| .lp-cta-note { | |
| margin: 0; | |
| font-size: 0.82rem; | |
| color: var(--text-dim); | |
| } | |
| /* ---------- Product shot ---------- */ | |
| .lp-shot { | |
| margin: 44px 0 0; | |
| } | |
| .lp-shot img { | |
| display: block; | |
| width: 100%; | |
| height: auto; | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-lg); | |
| box-shadow: var(--shadow-md); | |
| background: var(--bg-subtle); | |
| } | |
| .lp-shot figcaption { | |
| margin-top: 10px; | |
| font-size: 0.78rem; | |
| color: var(--text-dim); | |
| text-align: center; | |
| } | |
| .lp-shot-inset { margin: 0; } | |
| .lp-shot-inset figcaption { text-align: left; } | |
| /* ---------- Facts strip ---------- */ | |
| /* Four checkable statements about the product, in place of the customer | |
| logos a page like this usually carries — there are no customers to | |
| cite, and inventing them is the one thing that would make the page | |
| less trustworthy rather than more. */ | |
| .lp-facts { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 1px; | |
| max-width: var(--lp-max); | |
| margin: 64px auto 0; | |
| background: var(--border); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| overflow: hidden; | |
| } | |
| .lp-facts > div { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| padding: 22px 18px; | |
| background: var(--bg-panel); | |
| text-align: center; | |
| } | |
| .lp-facts strong { | |
| font-size: 1.9rem; | |
| font-weight: 800; | |
| color: var(--accent); | |
| letter-spacing: -0.02em; | |
| font-variant-numeric: tabular-nums; | |
| } | |
| .lp-facts span { | |
| font-size: 0.78rem; | |
| color: var(--text-dim); | |
| line-height: 1.4; | |
| } | |
| /* ---------- Sections ---------- */ | |
| .lp-section { | |
| max-width: var(--lp-max); | |
| margin: 0 auto; | |
| padding: 84px 24px 0; | |
| } | |
| .lp-h2 { | |
| margin: 0 0 12px; | |
| font-size: clamp(1.5rem, 3vw, 2.2rem); | |
| line-height: 1.15; | |
| letter-spacing: -0.02em; | |
| font-weight: 800; | |
| color: var(--accent); | |
| } | |
| .lp-h3 { | |
| margin: 0 0 10px; | |
| font-size: 1.05rem; | |
| font-weight: 700; | |
| color: var(--accent); | |
| } | |
| .lp-sub { | |
| margin: 0 0 32px; | |
| max-width: 60ch; | |
| color: var(--text-dim); | |
| line-height: 1.6; | |
| } | |
| .lp-body { | |
| margin: 0 0 20px; | |
| max-width: 54ch; | |
| color: var(--text-dim); | |
| line-height: 1.65; | |
| } | |
| /* ---------- Modes grid ---------- */ | |
| .lp-modes { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 14px; | |
| margin: 0; | |
| padding: 0; | |
| list-style: none; | |
| } | |
| .lp-modes li { | |
| padding: 20px; | |
| background: var(--bg-panel); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s; | |
| } | |
| .lp-modes li:hover { | |
| border-color: var(--border-strong); | |
| box-shadow: var(--shadow-sm); | |
| transform: translateY(-2px); | |
| } | |
| .lp-modes svg { | |
| width: 22px; | |
| height: 22px; | |
| color: var(--accent); | |
| margin-bottom: 12px; | |
| } | |
| .lp-modes h4 { | |
| margin: 0 0 6px; | |
| font-size: 0.98rem; | |
| font-weight: 700; | |
| color: var(--text); | |
| } | |
| .lp-modes p { | |
| margin: 0; | |
| font-size: 0.85rem; | |
| line-height: 1.55; | |
| color: var(--text-dim); | |
| } | |
| /* ---------- Split section ---------- */ | |
| .lp-split { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 48px; | |
| align-items: center; | |
| } | |
| .lp-checks { | |
| margin: 0; | |
| padding: 0; | |
| list-style: none; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| .lp-checks li { | |
| position: relative; | |
| padding-left: 28px; | |
| font-size: 0.9rem; | |
| line-height: 1.55; | |
| color: var(--text-dim); | |
| } | |
| .lp-checks li::before { | |
| content: ""; | |
| position: absolute; | |
| left: 4px; | |
| top: 6px; | |
| width: 6px; | |
| height: 11px; | |
| border: solid var(--success); | |
| border-width: 0 2px 2px 0; | |
| transform: rotate(45deg); | |
| } | |
| /* ---------- Steps ---------- */ | |
| .lp-steps { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 20px; | |
| margin: 0; | |
| padding: 0; | |
| list-style: none; | |
| counter-reset: none; | |
| } | |
| .lp-steps li { | |
| padding: 24px 20px; | |
| background: var(--bg-panel-2); | |
| border-radius: var(--radius); | |
| } | |
| .lp-step-n { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 30px; | |
| height: 30px; | |
| margin-bottom: 14px; | |
| border-radius: 50%; | |
| background: var(--accent); | |
| color: #ffffff; | |
| font-size: 0.85rem; | |
| font-weight: 700; | |
| } | |
| .lp-steps h4 { | |
| margin: 0 0 6px; | |
| font-size: 1rem; | |
| font-weight: 700; | |
| color: var(--text); | |
| } | |
| .lp-steps p { | |
| margin: 0; | |
| font-size: 0.87rem; | |
| line-height: 1.55; | |
| color: var(--text-dim); | |
| } | |
| /* ---------- Honest-limits block ---------- */ | |
| /* Deliberately quiet and late in the page, but present: the app states a | |
| limitation at every point it makes a claim, and a landing page that | |
| dropped that voice would be overselling what the tool does. */ | |
| .lp-honest { | |
| max-width: 820px; | |
| } | |
| .lp-honest p { | |
| margin: 0; | |
| padding: 20px 22px; | |
| background: var(--bg-panel-2); | |
| border-left: 3px solid var(--warning); | |
| border-radius: var(--radius-sm); | |
| font-size: 0.87rem; | |
| line-height: 1.65; | |
| color: var(--text-dim); | |
| } | |
| /* ---------- Final CTA + footer ---------- */ | |
| .lp-final { | |
| max-width: var(--lp-max); | |
| margin: 96px auto 0; | |
| padding: 64px 24px; | |
| text-align: center; | |
| background: var(--bg-panel-2); | |
| border-top: 1px solid var(--border); | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .lp-final p { | |
| margin: 0 0 26px; | |
| color: var(--text-dim); | |
| } | |
| .lp-footer { | |
| display: flex; | |
| flex-wrap: wrap; | |
| align-items: center; | |
| gap: 8px 18px; | |
| max-width: var(--lp-max); | |
| margin: 0 auto; | |
| padding: 28px 24px 44px; | |
| color: var(--text-dim); | |
| font-size: 0.82rem; | |
| } | |
| .lp-footer p { margin: 0; } | |
| /* ---------- Landing responsive ---------- */ | |
| @media (max-width: 900px) { | |
| .lp-modes { grid-template-columns: repeat(2, 1fr); } | |
| .lp-split { grid-template-columns: 1fr; gap: 32px; } | |
| .lp-facts { grid-template-columns: repeat(2, 1fr); } | |
| .lp-steps { grid-template-columns: 1fr; } | |
| } | |
| @media (max-width: 620px) { | |
| /* The section anchors are the first thing to go: the sticky bar has to | |
| keep the brand and the CTA, and those are what the page is for. */ | |
| .lp-nav-links { display: none; } | |
| .lp-nav { padding: 10px 16px; } | |
| .lp-hero { padding-top: 36px; } | |
| .lp-section { padding: 56px 16px 0; } | |
| .lp-modes { grid-template-columns: 1fr; } | |
| .lp-cta-row .btn { flex: 1 1 100%; justify-content: center; } | |
| .lp-final { margin-top: 64px; padding: 48px 16px; } | |
| .lp-footer { padding: 24px 16px 36px; } | |
| } | |
| /* The app header's brand doubles as the way back to the landing page, so it | |
| has to be a real button — but it must not look like one sitting in the | |
| header, so it inherits the brand's own type and colour. */ | |
| .brand-home { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 2px 4px; | |
| margin: -2px -4px; | |
| border: none; | |
| background: none; | |
| color: inherit; | |
| font: inherit; | |
| cursor: pointer; | |
| border-radius: var(--radius-sm); | |
| } | |
| .brand-home:hover { background: var(--accent-soft); } | |
| .brand-home:focus-visible { | |
| outline: 2px solid var(--accent); | |
| outline-offset: 2px; | |
| } | |
| /* Rotate control: an icon button that carries its current angle as a badge, | |
| so a rotated source is obvious from the toolbar without reading the frame. */ | |
| .rotate-btn { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .rotate-btn.is-active { | |
| color: var(--accent); | |
| border-color: var(--accent); | |
| background: var(--accent-soft); | |
| } | |
| .rotation-badge { | |
| font-size: 0.7rem; | |
| font-weight: 700; | |
| font-variant-numeric: tabular-nums; | |
| padding: 1px 5px; | |
| border-radius: 999px; | |
| background: var(--accent); | |
| color: #ffffff; | |
| } | |
| /* The <video> keeps playing and keeps feeding the detectors; it just stops | |
| being what you look at once the frame is rotated (the overlay canvas paints | |
| the rotated frame instead). Opacity rather than display:none so playback | |
| and layout are untouched. */ | |
| .stage-frame.source-rotated #video { opacity: 0; } | |
| /* Conditional controls must stay hidden when component styles set display. */ | |
| [hidden] { display: none ; } | |
| .workspace-guide { display:flex; align-items:center; justify-content:space-between; gap:18px; padding:5px 2px 8px; } | |
| .workspace-eyebrow { font-size:10px; font-weight:750; letter-spacing:.13em; color:var(--text-dim); } | |
| .workspace-guide h2 { margin:5px 0; font-size:20px; letter-spacing:-.025em; } | |
| .workspace-guide p { margin:0; font-size:12px; color:var(--text-dim); line-height:1.5; } | |
| .mode-picker { display:flex; flex-direction:column; gap:5px; flex-shrink:0; font-size:11px; color:var(--text-dim); } | |
| .mode-picker select { min-height:40px; min-width:130px; padding:8px 12px; border:1px solid var(--border); border-radius:6px; background:white; color:var(--text); } | |
| .workspace-controls { display:flex; align-items:center; gap:10px; min-height:52px; padding:8px 10px; border:1px solid var(--border); border-radius:7px; background:linear-gradient(180deg,#fff,#f9fbfd); box-shadow:var(--shadow-xs); } | |
| .workspace-controls-title { flex:0 0 auto; color:var(--text-dim); font-size:10px; font-weight:750; letter-spacing:.08em; text-transform:uppercase; } | |
| .workspace-chip { display:inline-flex; flex:0 0 auto; align-items:center; gap:6px; min-height:32px; padding:5px 9px; border:1px solid var(--border); border-radius:5px; background:#fff; color:#34445a; font-size:11px; font-weight:600; white-space:nowrap; } | |
| .workspace-toggle { cursor:pointer; user-select:none; transition:border-color .12s, background .12s, color .12s; } | |
| .workspace-toggle:has(input:checked) { border-color:#b7c6d9; background:#eef3f9; color:#142c4c; } | |
| .workspace-chip input[type="checkbox"] { width:14px; height:14px; margin:0; } | |
| .workspace-chip input[type="checkbox"]:checked::after { left:3px; top:0px; width:4px; height:8px; } | |
| .workspace-chip input[type="range"] { width:108px; height:18px; margin:0; accent-color:var(--accent); } | |
| .workspace-range-value { min-width:28px; color:var(--accent); text-align:right; font-variant-numeric:tabular-nums; } | |
| .workspace-select { gap:8px; } | |
| .workspace-select select { min-height:26px; padding:3px 6px; border:1px solid var(--border); border-radius:4px; background:white; color:var(--text); font-size:11px; } | |
| .workspace-toggle-list { display:flex; flex:1 1 auto; align-items:center; gap:6px; min-width:0; overflow-x:auto; scrollbar-width:thin; } | |
| .workspace-global-labels { margin-left:auto; } | |
| .workspace-controls [type="range"]:focus-visible, | |
| .workspace-controls select:focus-visible, | |
| .workspace-controls input[type="checkbox"]:focus-visible { outline:2px solid var(--accent); outline-offset:2px; } | |
| .video-review-controls { display:flex; align-items:center; gap:8px; min-height:48px; padding:6px 9px; border:1px solid var(--border); border-radius:7px; background:#f8fafc; } | |
| .video-review-controls .btn { min-height:32px; padding:5px 10px; font-size:11px; } | |
| .video-review-controls .video-step { min-width:42px; font-variant-numeric:tabular-nums; } | |
| .video-play-toggle { min-width:60px; } | |
| .video-scrubber-wrap { display:flex; flex:1 1 auto; min-width:60px; align-items:center; } | |
| .video-scrubber-wrap input { width:100%; margin:0; accent-color:var(--accent); } | |
| .video-time-readout { min-width:88px; color:var(--text-dim); text-align:center; font-size:11px; font-variant-numeric:tabular-nums; } | |
| .video-speed-wrap { display:flex; flex:0 0 auto; align-items:center; gap:6px; color:var(--text-dim); font-size:10px; font-weight:600; } | |
| .video-speed-wrap select { min-height:30px; padding:4px 6px; border:1px solid var(--border); border-radius:4px; background:white; color:var(--text); font-size:11px; } | |
| .video-annotation-hint { margin:-5px 2px 0; color:var(--text-dim); font-size:10px; line-height:1.35; } | |
| .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; } | |
| .bim-baseline-tools { display:flex; gap:7px; margin:0 0 4px; } | |
| .bim-baseline-tools .btn { flex:1 1 auto; min-height:34px; padding:6px 9px; font-size:11px; } | |
| #bimBaselineNote { margin:4px 0 12px; padding:8px 10px; border:1px solid var(--border); border-radius:5px; background:#f8fafc; line-height:1.45; } | |
| .stage-frame:not(.has-source) #video { visibility:hidden; } | |
| .stage-frame:not(.has-source) { background-color:#101d30; } | |
| .stage-toolbar { flex-wrap:wrap; } | |
| .source-name { max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--text-dim); font-size:12px; flex-basis:100%; } | |
| .source-feedback { padding:11px 14px; border:1px solid #b8cce5; border-radius:6px; background:#eff5fc; color:#153b67; font-size:13px; line-height:1.5; overflow-wrap:anywhere; } | |
| .source-feedback[data-tone="error"] { color:#982b2b; background:#fff2f2; border-color:#edcaca; } | |
| .source-feedback[data-tone="quality"] { color:#77540a; background:#fff8df; border-color:#ead99f; } | |
| .safety-quality-notice { margin:10px 0; padding:10px 12px; border:1px solid #cbd9e8; border-left:4px solid #6d86a6; border-radius:5px; background:#f3f7fb; color:#34475f; font-size:12px; line-height:1.5; } | |
| .safety-quality-notice.limited { border-color:#ead99f; border-left-color:#c18a12; background:#fff8df; color:#654908; } | |
| .mode-picker select:focus-visible { outline:2px solid var(--accent); outline-offset:3px; } | |
| @media (max-width:620px) { | |
| .workspace-guide { align-items:flex-start; flex-direction:column; gap:10px; } | |
| .workspace-guide h2 { font-size:19px; } | |
| .mode-picker { width:100%; flex-direction:row; align-items:center; justify-content:space-between; } | |
| .mode-picker select { min-width:170px; } | |
| .workspace-controls { align-items:flex-start; flex-wrap:wrap; gap:6px; padding:8px; } | |
| .workspace-controls-title { align-self:center; margin-right:2px; } | |
| .workspace-toggle-list { order:5; flex-basis:100%; padding-bottom:2px; } | |
| .workspace-global-labels { margin-left:auto; } | |
| .workspace-chip { min-height:36px; } | |
| .workspace-chip input[type="range"] { width:94px; } | |
| .video-review-controls { display:grid; grid-template-columns:auto auto minmax(44px,1fr) auto auto; gap:5px; padding:6px; } | |
| .video-speed-wrap { grid-column:1 / -1; justify-content:flex-end; } | |
| .video-time-readout { min-width:74px; font-size:10px; } | |
| .video-review-controls .btn { min-height:40px; } | |
| .bim-baseline-tools .btn { min-height:42px; } | |
| .stage-frame:not(.has-source) { min-height:360px; } | |
| .stage-toolbar .btn { min-height:44px; } | |
| } | |
| .measurement-summary { margin:12px 0 8px; padding:10px 12px; background:var(--accent-soft); border-radius:6px; font-size:12px; line-height:1.5; font-weight:600; color:var(--accent); } | |
| #measurementList { max-height:320px; overflow-y:auto; } | |
| /* ---------- Toggles, v2 ---------- | |
| Toolbar chips: the checkbox inside each chip is visually hidden and the | |
| chip itself carries the state — filled tint + solid mark when on, plain | |
| outline + hollow mark when off — so on/off reads at a glance from across | |
| the toolbar instead of from a 14px tick. The mark takes the layer's own | |
| overlay colour (--chip-color, copied from the panel's legend swatch), so | |
| a chip names the boxes it controls on the image. Square corners throughout, | |
| matching the flat navy theme. */ | |
| .workspace-toggle { position: relative; gap: 7px; color: var(--text-dim); font-weight: 600; } | |
| .workspace-toggle > input[type="checkbox"] { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; margin: 0; cursor: pointer; border: 0; } | |
| .workspace-toggle > input[type="checkbox"]::after { display: none; } | |
| .chip-mark { | |
| flex: 0 0 auto; width: 10px; height: 10px; border-radius: 2px; | |
| border: 1.5px solid var(--chip-color, #8a98ab); background: transparent; | |
| transition: background-color .12s, border-color .12s, box-shadow .12s; | |
| } | |
| .workspace-toggle:hover { border-color: #9fb3cc; color: var(--text); } | |
| .workspace-toggle:has(input:checked) { border-color: #9fb3cc; background: var(--accent-soft); color: var(--accent); } | |
| .workspace-toggle:has(input:checked) .chip-mark { background: var(--chip-color, var(--accent)); border-color: var(--chip-color, var(--accent)); box-shadow: inset 0 0 0 1.5px rgba(255,255,255,.55); } | |
| .workspace-toggle:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; } | |
| .workspace-toggle:active { transform: translateY(1px); } | |
| /* Named groups (Job Site: Detect / Safety flags / Experimental). On a wide | |
| screen the grouped list takes its own full-width row and wraps, so every | |
| chip is visible; each group is a labelled cluster. */ | |
| .workspace-controls:has(.workspace-toggle-list.grouped) { flex-wrap: wrap; row-gap: 8px; } | |
| .workspace-toggle-list.grouped { flex-basis: 100%; flex-wrap: wrap; overflow: visible; gap: 6px 14px; padding-top: 8px; border-top: 1px solid var(--border); } | |
| .workspace-toggle-group { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; } | |
| .workspace-group-name { flex: 0 0 auto; margin-right: 2px; color: var(--text-dim); font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; } | |
| .workspace-toggle-group + .workspace-toggle-group { padding-left: 14px; border-left: 1px solid var(--border); } | |
| /* A sideways-scrolling row (phones) fades out at whichever edge has more, | |
| so a chip cut off at the edge reads as "more this way". */ | |
| .workspace-toggle-list.fade-right { -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent); mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent); } | |
| .workspace-toggle-list.fade-left { -webkit-mask-image: linear-gradient(90deg, transparent, #000 28px); mask-image: linear-gradient(90deg, transparent, #000 28px); } | |
| .workspace-toggle-list.fade-left.fade-right { -webkit-mask-image: linear-gradient(90deg, transparent, #000 28px, #000 calc(100% - 28px), transparent); mask-image: linear-gradient(90deg, transparent, #000 28px, #000 calc(100% - 28px), transparent); } | |
| /* Slider chip: value reads as part of the control, not a stray number. */ | |
| .workspace-chip:has(input[type="range"]) { gap: 9px; } | |
| .workspace-range-value { padding: 2px 6px; border-radius: 3px; background: var(--accent-soft); font-size: 11px; font-weight: 700; } | |
| /* Sidebar on/off rows → switches. Label first, the switch in a consistent | |
| right-hand column, so a long list of layers and safety flags scans as | |
| "what" then "on/off" instead of a ragged column of ticks. Square-cornered | |
| track and knob to match the flat theme. Applies to layer/flag rows only; | |
| checkboxes elsewhere keep the square tick. */ | |
| .class-filter label, .field.checkbox { cursor: pointer; } | |
| .class-filter label > input[type="checkbox"], | |
| .field.checkbox > input[type="checkbox"] { | |
| order: 3; margin-left: auto; | |
| width: 30px; height: 17px; border: 0; border-radius: 3px; | |
| background: #c3ccd8; transition: background-color .15s; | |
| } | |
| .class-filter label > input[type="checkbox"]::after, | |
| .field.checkbox > input[type="checkbox"]::after { | |
| content: ''; position: absolute; left: 2px; top: 2px; width: 13px; height: 13px; | |
| border: 0; border-radius: 2px; background: #fff; transform: none; | |
| box-shadow: 0 1px 2px rgba(15, 37, 71, .25); transition: transform .15s; | |
| } | |
| .class-filter label > input[type="checkbox"]:checked, | |
| .field.checkbox > input[type="checkbox"]:checked { background: var(--accent); } | |
| .class-filter label > input[type="checkbox"]:checked::after, | |
| .field.checkbox > input[type="checkbox"]:checked::after { left: 2px; top: 2px; width: 13px; height: 13px; border: 0; transform: translateX(13px); } | |
| .class-filter label > input[type="checkbox"]:hover:not(:checked), | |
| .field.checkbox > input[type="checkbox"]:hover:not(:checked) { background: #aab6c6; } | |
| .class-filter label { gap: 10px; } | |
| .toggle-group > label + label { border-top: 1px solid rgba(221, 227, 236, .8); } | |
| .class-filter label:has(input:not(:checked)) { color: var(--text-dim); } | |
| .field.checkbox > span { flex: 1 1 auto; } | |
| @media (prefers-reduced-motion: reduce) { | |
| .class-filter label > input[type="checkbox"]::after, .field.checkbox > input[type="checkbox"]::after, .chip-mark { transition: none; } | |
| } | |
| /* Header: a long model status ("Loading object detection model…") pushed | |
| the mode tabs off the edge ("Dron"). It truncates; the full text is in | |
| its title. */ | |
| .model-status { max-width: 190px; overflow: hidden; text-overflow: ellipsis; display: inline-block; } | |
| .mode-switch { min-width: 0; } | |
| @media (max-width: 860px) { | |
| .class-filter label > input[type="checkbox"], | |
| .field.checkbox > input[type="checkbox"] { width: 38px; height: 22px; } | |
| .class-filter label > input[type="checkbox"]::after, | |
| .field.checkbox > input[type="checkbox"]::after, | |
| .class-filter label > input[type="checkbox"]:checked::after, | |
| .field.checkbox > input[type="checkbox"]:checked::after { width: 18px; height: 18px; } | |
| .class-filter label > input[type="checkbox"]:checked::after, | |
| .field.checkbox > input[type="checkbox"]:checked::after { transform: translateX(16px); } | |
| } | |
| @media (max-width: 620px) { | |
| /* One sideways row on a phone (13 chips would wrap to five rows); groups | |
| stay inline, separated by their names. */ | |
| .workspace-toggle-list.grouped { flex-wrap: nowrap; overflow-x: auto; padding-top: 6px; } | |
| .workspace-toggle-group { flex-wrap: nowrap; } | |
| } | |
| /* ---------- Robot bridge panel ---------- */ | |
| .robot-advisory { margin: 0 0 12px; padding: 8px 10px; border: 1px solid #ead99f; border-left: 4px solid #c18a12; border-radius: 5px; background: #fff8df; color: #654908; font-size: 0.75rem; line-height: 1.45; } | |
| .robot-fields { display: grid; gap: 8px; margin: 0 0 10px; } | |
| .robot-fields .field { margin: 0; font-size: 0.75rem; color: var(--text-dim); } | |
| .robot-fields input, .robot-fields select { min-height: 32px; padding: 5px 8px; border: 1px solid var(--border); border-radius: 4px; background: var(--bg-panel); color: var(--text); font: inherit; font-size: 0.8rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } | |
| .robot-fields select { font-family: inherit; } | |
| .robot-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } | |
| .robot-status { display: flex; align-items: center; gap: 7px; margin: 8px 0 2px; font-size: 0.78rem; font-weight: 600; color: var(--text-dim); overflow-wrap: anywhere; } | |
| .robot-status::before { content: ''; flex: 0 0 8px; height: 8px; border-radius: 2px; background: #aab6c6; } | |
| .robot-status[data-state="connected"] { color: var(--success); } | |
| .robot-status[data-state="connected"]::before { background: var(--success); } | |
| .robot-status[data-state="connecting"]::before { background: var(--accent); } | |
| .robot-status[data-state="retrying"], .robot-status[data-state="error"] { color: #982b2b; } | |
| .robot-status[data-state="retrying"]::before, .robot-status[data-state="error"]::before { background: var(--danger); } | |
| #robotPanel code { font-size: 0.72rem; padding: 0 3px; border-radius: 3px; background: var(--bg-subtle); overflow-wrap: anywhere; } | |
| #rosDownloadBtn { margin-top: 6px; } | |
| @media (max-width: 860px) { .robot-fields input, .robot-fields select { font-size: 16px; min-height: 40px; } } | |
| .robot-fields .field, .robot-row > * { min-width: 0; } | |
| .robot-fields input, .robot-fields select { width: 100%; box-sizing: border-box; } | |
| /* ---------- Progress mode ---------- */ | |
| .progress-trades { display: grid; gap: 6px; margin: 4px 0 12px; } | |
| .progress-trade { display: grid; grid-template-columns: minmax(84px, 1fr) 2fr; grid-template-rows: auto auto; column-gap: 8px; align-items: center; font-size: 12px; } | |
| .progress-trade-name { font-weight: 600; color: var(--text); } | |
| .progress-bar { position: relative; height: 8px; background: var(--bg-subtle); border: 1px solid var(--border); border-radius: 2px; overflow: hidden; } | |
| .progress-bar i { position: absolute; inset: 0 auto 0 0; transition: width 0.3s ease; } | |
| .progress-trade-status { grid-column: 2; color: var(--text-dim); font-size: 11px; } | |
| .progress-plan { margin: 0 0 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 6px 8px; } | |
| .progress-plan summary { cursor: pointer; font-weight: 600; font-size: 12px; } | |
| .progress-plan-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 8px; } | |
| .progress-plan-grid .field { margin-bottom: 6px; } | |
| .progress-plan-grid input, .progress-plan-grid select { width: 100%; min-width: 0; } | |
| .progress-save { display: grid; gap: 4px; margin-bottom: 10px; } | |
| .progress-changes { font-size: 12px; margin-bottom: 6px; } | |
| .progress-changes p { margin: 2px 0; } | |
| .progress-changes-head { color: var(--text-dim); } | |
| .progress-change.advanced { color: var(--success); } | |
| .progress-change.regressed { color: var(--danger); } | |
| .progress-timeline { list-style: none; margin: 0 0 10px; padding: 0; display: grid; gap: 6px; max-height: 260px; overflow-y: auto; } | |
| .progress-timeline li { display: flex; gap: 8px; align-items: center; font-size: 12px; padding: 4px; border: 1px solid var(--border); border-radius: var(--radius-sm); } | |
| .progress-timeline img { width: 64px; height: 48px; object-fit: cover; border-radius: 2px; flex: none; } | |
| .progress-actions { display: grid; gap: 6px; } | |
| .progress-confirm { font-size: 12px; margin: 0; } | |
| /* Progress → Project: schedule status, all-areas dashboard, floor plan with pins */ | |
| .progress-project { margin: 10px 0 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 6px 8px; } | |
| .progress-project > summary { cursor: pointer; font-weight: 600; font-size: 12px; } | |
| .progress-project > .field { margin-bottom: 6px; } | |
| .progress-project > .field input { width: 100%; min-width: 0; } | |
| .progress-sched-status { font-size: 12px; margin: 0 0 8px; padding-left: 8px; border-left: 3px solid var(--c, transparent); color: var(--text); } | |
| .progress-sched-status:empty { display: none; } | |
| .progress-dashboard { font-size: 12px; margin-bottom: 8px; } | |
| .progress-kpis { margin: 0 0 6px; color: var(--text-dim); } | |
| .progress-attention { list-style: none; margin: 0 0 8px; padding: 0; display: grid; gap: 4px; } | |
| .progress-attention li { display: grid; grid-template-columns: auto 1fr; column-gap: 6px; align-items: baseline; } | |
| .progress-attention-why { grid-column: 1 / -1; color: var(--text-dim); font-size: 11px; } | |
| .progress-status-tag { font-size: 11px; padding: 0 6px; border: 1px solid var(--c); color: var(--c); border-radius: 9px; white-space: nowrap; } | |
| .progress-area-link { background: none; border: 0; padding: 0; font: inherit; font-weight: 600; color: var(--text); text-align: left; cursor: pointer; text-decoration: underline; text-underline-offset: 2px; min-width: 0; overflow-wrap: anywhere; } | |
| .progress-floors { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; } | |
| .progress-floors th, .progress-floors td { text-align: left; padding: 2px 4px; border-bottom: 1px solid var(--border); } | |
| .progress-floors th { color: var(--text-dim); font-weight: 600; font-size: 11px; } | |
| .progress-floorplan { margin: 6px 0 8px; } | |
| .progress-plan-upload { display: block; width: 100%; text-align: center; margin-bottom: 6px; cursor: pointer; box-sizing: border-box; } | |
| .progress-plan-canvas { display: block; width: 100%; border: 1px solid var(--border); border-radius: var(--radius-sm); cursor: crosshair; margin-top: 4px; } | |
| .progress-plan-canvas[hidden] { display: none; } | |
| /* BIM → Facade progress by floor */ | |
| .facade-floors { display: grid; gap: 3px; margin: 2px 0 8px; max-height: 320px; overflow-y: auto; } | |
| .facade-floor { display: grid; grid-template-columns: 58px 1fr auto; gap: 6px; align-items: center; font-size: 11px; } | |
| .facade-floor-name { font-weight: 600; color: var(--text); } | |
| .facade-floor-status { color: var(--text-dim); white-space: nowrap; } | |
| /* Progress → Project: curve, schedule import, project files */ | |
| .progress-curve { position: relative; margin: 0 0 8px; color: var(--text); } | |
| .progress-curve:empty { display: none; } | |
| .progress-project-files { display: grid; gap: 6px; margin-top: 8px; } | |
| .progress-file-btn { display: block; text-align: center; cursor: pointer; box-sizing: border-box; } | |
| .schedule-preview { font-size: 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 6px 8px; display: grid; gap: 4px; } | |
| .schedule-preview ul { margin: 0; padding-left: 16px; color: var(--text-dim); max-height: 120px; overflow-y: auto; } | |
| .schedule-preview .schedule-actions { display: flex; gap: 6px; } | |
| .survey-preview { margin: 4px 0 8px; border: 1px solid var(--border); border-radius: var(--radius-sm); } | |
| .survey-preview:empty { display: none; } | |
| /* AI assistant */ | |
| .ai-key-row { display: flex; flex-wrap: wrap; gap: 4px 8px; align-items: center; margin-bottom: 4px; } | |
| .ai-key-row .field.checkbox { margin: 0; flex: 1 1 auto; } | |
| .ai-conversation { display: grid; gap: 8px; margin: 4px 0 8px; max-height: 420px; overflow-y: auto; } | |
| .ai-conversation:empty { display: none; } | |
| .ai-msg { font-size: 13px; line-height: 1.45; padding: 6px 8px; border-radius: var(--radius-sm); border: 1px solid var(--border); white-space: pre-wrap; overflow-wrap: anywhere; } | |
| .ai-msg.user { background: var(--bg-subtle); } | |
| .ai-msg.error { border-color: var(--danger); color: var(--danger); } | |
| .ai-msg .ai-meta { display: block; margin-top: 4px; font-size: 11px; color: var(--text-dim); white-space: normal; } | |
| .ai-actions { display: flex; gap: 6px; margin-bottom: 6px; } | |
| .ai-actions .btn-primary { flex: 1; } | |
| #aiQuestion { width: 100%; box-sizing: border-box; resize: vertical; font: inherit; } | |
| /* Progress → Video walkthrough */ | |
| .walk-seg-suggested { font-size: 11px; color: var(--text-dim); margin-top: -3px; } | |
| .progress-trade-dates { margin: 0 0 8px; } | |
| .progress-trade-dates > summary { cursor: pointer; font-size: 12px; font-weight: 600; } | |
| .progress-trade-date-rows { display: grid; gap: 4px; } | |
| .progress-trade-date-row { display: grid; grid-template-columns: minmax(70px, 1fr) 1fr 1fr; gap: 4px; align-items: center; font-size: 11px; } | |
| .progress-trade-date-row input { min-width: 0; width: 100%; box-sizing: border-box; font-size: 11px; } | |
| .progress-scale-row { font-size: 12px; margin: 0 0 6px; } | |
| .progress-scale-row input { width: 80px; } | |
| .progress-quantities { margin: 0 0 6px; color: var(--text-dim); } | |
| .walk-buttons { display: flex; gap: 6px; align-items: flex-end; } | |
| .walk-buttons .btn { flex: 1; } | |
| .walk-progress { width: 100%; height: 6px; margin: 0 0 6px; } | |
| .walk-progress[hidden] { display: none; } | |
| .walk-segments { list-style: none; margin: 0 0 8px; padding: 0; display: grid; gap: 8px; } | |
| .walk-seg { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 6px; display: grid; gap: 6px; } | |
| .walk-seg-head { display: flex; gap: 8px; align-items: flex-start; font-size: 12px; } | |
| .walk-seg-head img { width: 96px; height: 54px; object-fit: cover; border-radius: 2px; flex: none; } | |
| .walk-seg-head div { display: grid; gap: 2px; min-width: 0; } | |
| .walk-seg-summary { color: var(--text-dim); font-size: 11px; } | |
| .walk-seg-name { width: 100%; min-width: 0; box-sizing: border-box; } | |
| .walk-seg-actions:empty { display: none; } | |
| .walk-strip { display: flex; gap: 3px; overflow-x: auto; padding-bottom: 2px; } | |
| .walk-strip button { padding: 0; border: 1px solid var(--border); border-radius: 2px; background: none; cursor: pointer; flex: none; line-height: 0; } | |
| .walk-strip button:disabled { cursor: default; outline: 2px solid var(--accent); outline-offset: -2px; } | |
| .walk-strip img { width: 48px; height: 27px; object-fit: cover; } | |
| @media (prefers-reduced-motion: reduce) { .progress-bar i { transition: none; } } | |