: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 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 !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } .spinner { animation-duration: 1.6s !important; animation-iteration-count: infinite !important; } } /* ============================================================ 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
; the landing is a normal scrolling document, so 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