Spaces:
Running
Running
| .loading-screen { | |
| position: fixed; | |
| inset: 0; | |
| z-index: 30; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 20px; | |
| background: radial-gradient(circle at 50% 42%, rgba(0, 122, 255, 0.22), rgba(0, 0, 0, 0.88) 58%); | |
| } | |
| .loading-screen.hidden, | |
| .hidden { | |
| display: none ; | |
| } | |
| .loading-card { | |
| width: clamp(288px, 92vw, 420px); | |
| padding: 24px; | |
| pointer-events: auto; | |
| text-align: center; | |
| } | |
| .loading-ring { | |
| width: 96px; | |
| height: 96px; | |
| margin: 0 auto 18px; | |
| border: 2px solid rgba(255, 255, 255, 0.16); | |
| border-top-color: var(--accent); | |
| border-radius: 50%; | |
| animation: spin 1.2s linear infinite; | |
| } | |
| .caption { | |
| margin: 0 0 8px; | |
| color: var(--text-secondary); | |
| font: var(--font-caption); | |
| text-transform: uppercase; | |
| } | |
| .loading-card h1 { | |
| margin: 0 0 8px; | |
| font: var(--font-title1); | |
| } | |
| .loading-card p { | |
| margin: 0 0 18px; | |
| color: var(--text-secondary); | |
| font: var(--font-subheadline); | |
| } | |
| .progress-track { | |
| height: 4px; | |
| margin: 0 0 20px; | |
| overflow: hidden; | |
| border-radius: 99px; | |
| background: rgba(255, 255, 255, 0.12); | |
| } | |
| .progress-track span { | |
| display: block; | |
| width: 14%; | |
| height: 100%; | |
| border-radius: inherit; | |
| background: var(--accent); | |
| box-shadow: 0 0 18px var(--accent-glow); | |
| transition: width 0.25s ease; | |
| } | |
| .hud-toolbar { | |
| position: fixed; | |
| left: calc(12px + env(safe-area-inset-left)); | |
| right: calc(12px + env(safe-area-inset-right)); | |
| bottom: calc(14px + env(safe-area-inset-bottom)); | |
| z-index: 15; | |
| display: flex; | |
| justify-content: center; | |
| gap: 8px; | |
| padding: 10px; | |
| pointer-events: auto; | |
| overflow-x: auto; | |
| scrollbar-width: none; | |
| } | |
| .hud-toolbar::-webkit-scrollbar { | |
| display: none; | |
| } | |
| .status-panel { | |
| position: fixed; | |
| top: calc(12px + env(safe-area-inset-top)); | |
| left: calc(12px + env(safe-area-inset-left)); | |
| right: calc(12px + env(safe-area-inset-right)); | |
| z-index: 15; | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 8px; | |
| padding: 10px 12px; | |
| pointer-events: none; | |
| } | |
| .status-panel span { | |
| display: block; | |
| margin-bottom: 3px; | |
| color: var(--text-tertiary); | |
| font: var(--font-caption); | |
| letter-spacing: 0.18em; | |
| text-transform: uppercase; | |
| } | |
| .status-panel strong { | |
| display: block; | |
| overflow: hidden; | |
| color: var(--accent); | |
| font: 500 12px/1.25 var(--font-mono); | |
| letter-spacing: 0.02em; | |
| text-overflow: ellipsis; | |
| text-transform: uppercase; | |
| white-space: nowrap; | |
| } | |
| .ar-panel { | |
| position: fixed; | |
| width: clamp(280px, 90vw, 400px); | |
| min-height: 220px; | |
| max-height: min(74vh, 640px); | |
| z-index: 12; | |
| padding: 18px; | |
| pointer-events: auto; | |
| transform: translate3d(16px, 96px, 0); | |
| transition: box-shadow 0.18s ease, transform 0.08s ease; | |
| } | |
| .ar-panel.active { | |
| box-shadow: var(--glass-shadow-deep), 0 0 42px var(--accent-glow); | |
| } | |
| .ar-panel header { | |
| height: 44px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 12px; | |
| padding-left: 34px; | |
| } | |
| .ar-panel h2 { | |
| min-width: 0; | |
| margin: 0; | |
| overflow: hidden; | |
| font: var(--font-title3); | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .panel-minimize { | |
| width: 48px; | |
| height: 48px; | |
| flex: 0 0 48px; | |
| } | |
| .panel-body { | |
| max-height: calc(min(74vh, 640px) - 80px); | |
| overflow: auto; | |
| padding: 6px 2px 2px; | |
| scrollbar-width: thin; | |
| } | |
| .panel-body input, | |
| .panel-body select, | |
| .panel-body textarea { | |
| width: 100%; | |
| min-height: 46px; | |
| margin: 6px 0; | |
| padding: 12px 14px; | |
| border: 1px solid var(--separator); | |
| border-radius: 14px; | |
| outline: 0; | |
| background: rgba(0, 0, 0, 0.22); | |
| color: var(--text-primary); | |
| } | |
| .panel-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |
| gap: 8px; | |
| } | |
| .panel-row { | |
| display: flex; | |
| gap: 8px; | |
| align-items: center; | |
| } | |
| .panel-action { | |
| min-height: 46px; | |
| padding: 10px 12px; | |
| border: 1px solid var(--glass-border); | |
| border-radius: 14px; | |
| background: rgba(255, 255, 255, 0.08); | |
| color: var(--text-primary); | |
| cursor: pointer; | |
| } | |
| .panel-action:active { | |
| background: var(--glass-bg-active); | |
| transform: scale(0.98); | |
| } | |
| .panel-list { | |
| display: grid; | |
| gap: 8px; | |
| margin: 12px 0; | |
| } | |
| .model-card { | |
| display: grid; | |
| grid-template-columns: 1fr auto; | |
| gap: 8px; | |
| align-items: center; | |
| min-height: 54px; | |
| padding: 10px; | |
| border: 1px solid var(--separator); | |
| border-radius: 14px; | |
| background: rgba(255, 255, 255, 0.06); | |
| } | |
| .model-card strong, | |
| .model-card span { | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .model-card span { | |
| color: var(--text-secondary); | |
| font: var(--font-caption); | |
| } | |
| .sketchfab-panel { | |
| display: grid; | |
| gap: 8px; | |
| margin: 12px 0; | |
| padding: 12px; | |
| border: 1px solid var(--separator); | |
| border-radius: 16px; | |
| background: rgba(255, 255, 255, 0.05); | |
| } | |
| .sketchfab-panel strong { | |
| font: var(--font-headline); | |
| } | |
| .panel-note { | |
| margin: 0; | |
| color: var(--text-secondary); | |
| font: var(--font-footnote); | |
| } | |
| .inline-row { | |
| display: grid; | |
| grid-template-columns: minmax(0, 1fr) auto; | |
| gap: 8px; | |
| align-items: center; | |
| } | |
| .inline-row .panel-action { | |
| min-width: 72px; | |
| } | |
| .sketchfab-results { | |
| max-height: 280px; | |
| overflow: auto; | |
| padding-right: 2px; | |
| } | |
| .sketchfab-card { | |
| grid-template-columns: 68px minmax(0, 1fr); | |
| align-items: stretch; | |
| } | |
| .sketchfab-card img { | |
| width: 68px; | |
| height: 68px; | |
| align-self: center; | |
| border-radius: 12px; | |
| object-fit: cover; | |
| background: rgba(0, 0, 0, 0.32); | |
| } | |
| .sketchfab-card > div:first-of-type { | |
| min-width: 0; | |
| display: grid; | |
| gap: 2px; | |
| } | |
| .sketchfab-card a { | |
| min-width: 0; | |
| overflow: hidden; | |
| color: var(--text-secondary); | |
| font: var(--font-caption); | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .model-actions { | |
| grid-column: 1 / -1; | |
| display: grid; | |
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |
| gap: 8px; | |
| } | |
| .link-action { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| min-height: 46px; | |
| text-decoration: none; | |
| } | |
| /* Claude Design pass (Part 38): webar/assistant-bubble.html. | |
| assistant-frontend-experience: "Give every assistant state a real visual identity — | |
| listening, thinking/processing, speaking, idle." The bubble had ONE appearance for all | |
| of them, so it could never say which was happening. */ | |
| .jarvis-bubble { | |
| position: fixed; | |
| left: 50%; | |
| bottom: calc(98px + env(safe-area-inset-bottom)); | |
| z-index: 20; | |
| width: min(390px, calc(100vw - 24px)); | |
| padding: 13px 40px 13px 15px; | |
| /* Was `pointer-events: none`, which made a long reply impossible to scroll, select or | |
| dismiss — the text was visible and completely inert. */ | |
| pointer-events: auto; | |
| transform: translateX(-50%); | |
| } | |
| /* A long reply scrolls inside the bubble instead of growing without bound and pushing | |
| itself off-screen — the same class of overflow bug Part 30 found on Guardian. */ | |
| .jarvis-bubble p { | |
| max-height: 7.5em; | |
| overflow-y: auto; | |
| } | |
| .jarvis-bubble .bubble-x { | |
| position: absolute; | |
| top: 8px; | |
| right: 8px; | |
| width: 26px; | |
| height: 26px; | |
| display: grid; | |
| place-items: center; | |
| padding: 0; | |
| border: 1px solid rgba(255, 255, 255, 0.10); | |
| border-radius: 7px; | |
| background: rgba(255, 255, 255, 0.05); | |
| color: var(--text-secondary); | |
| font: 400 17px/1 var(--font); | |
| cursor: pointer; | |
| } | |
| .jarvis-bubble .bubble-x:hover, | |
| .jarvis-bubble .bubble-x:focus-visible { | |
| color: var(--text-primary); | |
| background: rgba(255, 255, 255, 0.12); | |
| } | |
| /* ---- state identities. Each is a different SHAPE and a different MOTION, so the state | |
| survives being seen in monochrome or by someone who cannot distinguish the hues. ---- */ | |
| .jarvis-bubble .state-glyph { | |
| display: inline-flex; | |
| align-items: flex-end; | |
| gap: 2px; | |
| height: 11px; | |
| margin-right: 6px; | |
| vertical-align: -1px; | |
| } | |
| .jarvis-bubble .state-glyph i { | |
| width: 2px; | |
| border-radius: 1px; | |
| background: currentColor; | |
| } | |
| /* LISTENING — an input meter that reacts, bars at differing heights */ | |
| .jarvis-bubble[data-state="listening"] span { color: var(--accent); } | |
| .jarvis-bubble[data-state="listening"] .state-glyph i { | |
| animation: jb-level 0.9s ease-in-out infinite; | |
| } | |
| .jarvis-bubble[data-state="listening"] .state-glyph i:nth-child(1) { height: 40%; animation-delay: 0s; } | |
| .jarvis-bubble[data-state="listening"] .state-glyph i:nth-child(2) { height: 80%; animation-delay: 0.12s; } | |
| .jarvis-bubble[data-state="listening"] .state-glyph i:nth-child(3) { height: 55%; animation-delay: 0.24s; } | |
| .jarvis-bubble[data-state="listening"] .state-glyph i:nth-child(4) { height: 95%; animation-delay: 0.36s; } | |
| @keyframes jb-level { 0%, 100% { transform: scaleY(0.35); } 50% { transform: scaleY(1); } } | |
| /* THINKING — an orbiting arc. Deliberately NOT a bar: a bar implies a known duration | |
| and the model's latency is not known in advance. */ | |
| .jarvis-bubble[data-state="thinking"] span { color: var(--warning, #ffb800); } | |
| .jarvis-bubble[data-state="thinking"] .state-glyph { | |
| width: 11px; | |
| height: 11px; | |
| border: 1.5px solid rgba(255, 184, 0, 0.22); | |
| border-top-color: var(--warning, #ffb800); | |
| border-radius: 50%; | |
| animation: jb-spin 0.85s linear infinite; | |
| } | |
| .jarvis-bubble[data-state="thinking"] .state-glyph i { display: none; } | |
| @keyframes jb-spin { to { transform: rotate(360deg); } } | |
| /* SPEAKING — a travelling waveform, matching voice output */ | |
| .jarvis-bubble[data-state="speaking"] span { color: var(--success, #32d74b); } | |
| .jarvis-bubble[data-state="speaking"] .state-glyph i { | |
| height: 100%; | |
| transform-origin: center; | |
| animation: jb-wave 1.1s ease-in-out infinite; | |
| } | |
| .jarvis-bubble[data-state="speaking"] .state-glyph i:nth-child(2) { animation-delay: 0.14s; } | |
| .jarvis-bubble[data-state="speaking"] .state-glyph i:nth-child(3) { animation-delay: 0.28s; } | |
| .jarvis-bubble[data-state="speaking"] .state-glyph i:nth-child(4) { animation-delay: 0.42s; } | |
| @keyframes jb-wave { 0%, 100% { transform: scaleY(0.25); } 50% { transform: scaleY(1); } } | |
| /* FAILED — no motion at all. Stillness is itself the signal, and it is the one state | |
| that must never look like work is still happening. */ | |
| .jarvis-bubble[data-state="failed"] { | |
| border-color: rgba(255, 59, 48, 0.45); | |
| } | |
| .jarvis-bubble[data-state="failed"] span { color: var(--destructive, #ff3b30); } | |
| .jarvis-bubble[data-state="failed"] .state-glyph i { height: 100%; animation: none; } | |
| @media (prefers-reduced-motion: reduce) { | |
| .jarvis-bubble .state-glyph, | |
| .jarvis-bubble .state-glyph i { | |
| animation-duration: 3s; | |
| } | |
| } | |
| .jarvis-bubble span { | |
| display: block; | |
| margin-bottom: 4px; | |
| color: var(--accent); | |
| font: 700 11px/1 var(--font); | |
| letter-spacing: 0.3em; | |
| text-transform: uppercase; | |
| } | |
| .jarvis-bubble p { | |
| margin: 0; | |
| color: var(--text-primary); | |
| font: var(--font-callout); | |
| } | |
| /* | |
| One fixed column holds both notice hosts, so notices stack instead of colliding. | |
| History, so the magic numbers don't come back: the hosts were positioned | |
| independently at top:78px and top:152px. The 78px was itself a fix — an error card | |
| used to land on top of the CAMERA / TRACKING / OMEGA readout and hide the very | |
| state a user needs while something is failing (.status-panel is fixed at top:12px, | |
| ~50px tall, so both hosts must clear ~78px). But 152px silently assumed an error | |
| card is never taller than 74px. A real one — the 8th Wall fallback-tracking | |
| message — measured 112px in a live session, so it ran under the wake-word banner | |
| and neither was fully readable. | |
| A column with a gap has no such assumption: the second notice starts wherever the | |
| first one actually ends, at any content length, in any language. | |
| */ | |
| #notice-stack { | |
| position: fixed; | |
| top: calc(78px + env(safe-area-inset-top)); | |
| left: 12px; | |
| right: 12px; | |
| /* z-index deliberately not set here — index.html owns the compositor layer order | |
| for every overlay, and two files competing for it is how layers drift. */ | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| pointer-events: none; | |
| } | |
| #error-host, | |
| #banner-host { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| pointer-events: none; | |
| } | |
| /* An empty host must not contribute its gap to the column. */ | |
| #error-host:empty, | |
| #banner-host:empty { | |
| display: none; | |
| } | |
| /* Claude Design pass (Part 38): webar/error-banner-host.html. | |
| Error vs notice was carried by border-colour and box-shadow ALONE. Now four | |
| independent signals — thick left edge, icon shape, the kind in words, colour last. */ | |
| .error-panel, | |
| .banner-panel { | |
| position: relative; | |
| display: flex; | |
| align-items: flex-start; | |
| gap: 11px; | |
| width: min(520px, calc(100vw - 24px)); | |
| margin: 0 auto 8px; | |
| padding: 12px 40px 12px 13px; /* right pad clears the dismiss control */ | |
| border-left-width: 3px; /* SIGNAL 1 — thick edge */ | |
| border-left-style: solid; | |
| pointer-events: auto; | |
| } | |
| .error-panel .panel-ico, | |
| .banner-panel .panel-ico { /* SIGNAL 2 — icon shape */ | |
| flex: none; | |
| width: 17px; | |
| height: 17px; | |
| margin-top: 1px; | |
| } | |
| .error-panel strong, | |
| .banner-panel strong { /* SIGNAL 3 — the kind, in words */ | |
| letter-spacing: 0.22em; | |
| text-transform: uppercase; | |
| } | |
| /* SIGNAL 4 — colour, last, and never alone. */ | |
| .error-panel { | |
| border-color: rgba(255, 59, 48, 0.45); | |
| border-left-color: var(--destructive, #ff3b30); | |
| box-shadow: var(--glass-shadow), 0 0 28px var(--destructive-glow); | |
| } | |
| .error-panel .panel-ico, | |
| .error-panel strong { | |
| color: var(--destructive, #ff3b30); | |
| } | |
| .banner-panel { | |
| border-left-color: var(--accent); | |
| } | |
| .banner-panel .panel-ico, | |
| .banner-panel strong { | |
| color: var(--accent); | |
| } | |
| /* The dismiss control. There was none: a panel created with `timeout: 0` was permanent | |
| and the user could never clear it. 24px target with a generous hit area. */ | |
| .panel-x { | |
| position: absolute; | |
| top: 8px; | |
| right: 8px; | |
| width: 26px; | |
| height: 26px; | |
| display: grid; | |
| place-items: center; | |
| padding: 0; | |
| border: 1px solid rgba(255, 255, 255, 0.10); | |
| border-radius: 7px; | |
| background: rgba(255, 255, 255, 0.05); | |
| color: var(--text-secondary); | |
| font: 400 17px/1 var(--font); | |
| cursor: pointer; | |
| } | |
| .panel-x:hover, | |
| .panel-x:focus-visible { | |
| color: var(--text-primary); | |
| background: rgba(255, 255, 255, 0.12); | |
| } | |
| .error-panel strong, | |
| .banner-panel strong { | |
| display: block; | |
| margin-bottom: 4px; | |
| font: var(--font-headline); | |
| } | |
| .error-panel p, | |
| .banner-panel p { | |
| margin: 0; | |
| color: var(--text-secondary); | |
| font: var(--font-footnote); | |
| } | |
| .error-panel a { | |
| color: var(--text-primary); | |
| } | |
| .context-menu { | |
| position: fixed; | |
| z-index: 22; | |
| display: grid; | |
| gap: 6px; | |
| width: 190px; | |
| padding: 8px; | |
| pointer-events: auto; | |
| } | |
| .context-menu button { | |
| min-height: 42px; | |
| border: 0; | |
| border-radius: 12px; | |
| background: rgba(255, 255, 255, 0.08); | |
| color: var(--text-primary); | |
| } | |
| .game-frame { | |
| width: 100%; | |
| height: 100%; | |
| border: 0; | |
| background: var(--bg-darker); | |
| } | |
| /* ============================================================================ | |
| Part 32 — Claude Design pass: WebAR | |
| ---------------------------------------------------------------------------- | |
| WebAR joined this flagship's tracked platforms in v16 and had never received a | |
| design pass. Part 31 recorded it as blocked because "ui/hud.css is not linked | |
| from index.html" — that premise is wrong: index.html:19 links it directly. | |
| Re-checked rather than carried forward, so the work went ahead. | |
| Two surfaces taken here, both chosen because every single session passes | |
| through them: the loading screen (the first thing any user ever sees) and the | |
| status panel (persistent for the whole AR session). | |
| Constraint that shapes all of it: this renders over a live camera feed on a | |
| mid-tier phone. Per spatial-design-principles.md, that means no heavy blur or | |
| post-processing in the compositing path, legible contrast against arbitrary | |
| real-world backdrops, and thumb-reachable primary actions. | |
| ========================================================================== */ | |
| /* ---- Loading screen ------------------------------------------------------ */ | |
| .loading-card { | |
| position: relative; | |
| animation: omega-load-in 420ms cubic-bezier(0.22, 1, 0.36, 1) both; | |
| } | |
| @keyframes omega-load-in { | |
| from { opacity: 0; transform: translateY(14px) scale(0.985); } | |
| to { opacity: 1; transform: none; } | |
| } | |
| /* The same HUD corner brackets used on the exe panels and the Guardian | |
| composables, so all three platforms read as one product. */ | |
| .loading-card::before, | |
| .loading-card::after { | |
| content: ''; | |
| position: absolute; | |
| width: 16px; | |
| height: 16px; | |
| pointer-events: none; | |
| border-color: var(--omega-accent, #00d4ff); | |
| opacity: 0.6; | |
| } | |
| .loading-card::before { | |
| top: 0; left: 0; | |
| border-top: 2px solid; border-left: 2px solid; | |
| } | |
| .loading-card::after { | |
| bottom: 0; right: 0; | |
| border-bottom: 2px solid; border-right: 2px solid; | |
| } | |
| .loading-card .caption { | |
| letter-spacing: 0.22em; | |
| font-size: 11px; | |
| opacity: 0.75; | |
| color: var(--omega-accent, #00d4ff); | |
| } | |
| .loading-card h1 { | |
| position: relative; | |
| padding-bottom: 10px; | |
| letter-spacing: 0.02em; | |
| } | |
| /* Centred on the title it underlines. This was `left: 0` inside a `text-align: center` | |
| card, so it rendered as a stray cyan dash floating at the left edge rather than as the | |
| title's rail — clearly visible in knowledge/screens/webar/webar_01_access_gate.png. */ | |
| .loading-card h1::after { | |
| content: ''; | |
| position: absolute; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| bottom: 0; | |
| width: 48px; | |
| height: 2px; | |
| background: var(--omega-accent, #00d4ff); | |
| box-shadow: 0 0 10px rgba(0, 212, 255, 0.55); | |
| animation: omega-load-rail 3.4s ease-in-out infinite; | |
| } | |
| @keyframes omega-load-rail { | |
| 0%, 100% { width: 48px; opacity: 0.9; } | |
| 50% { width: 70px; opacity: 1; } | |
| } | |
| /* Start AR is the single most important control in the product. It gets real | |
| presence and a thumb-reachable target — 48px minimum, per the touch-on-glass | |
| guidance: the user is holding a phone one-handed while walking. */ | |
| .loading-card .btn-primary { | |
| min-height: 48px; | |
| letter-spacing: 0.12em; | |
| transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease; | |
| /* .btn-primary inherits color:var(--text-primary) — near-white on the cyan fill, which | |
| measures about 1.9:1. On the single most important control in the product, viewed on a | |
| phone outdoors. Dark ink on the same cyan measures about 11:1. */ | |
| color: var(--bg-darker, #0A0A0F); | |
| font-weight: 700; | |
| } | |
| /* Disabled has to be visibly and actually disabled. A live-looking button that silently eats | |
| the tap is worse than no button: the user concludes the product is broken, not busy. */ | |
| .loading-card .btn-primary[disabled] { | |
| background: rgba(255, 255, 255, 0.10); | |
| color: rgba(255, 255, 255, 0.45); | |
| cursor: not-allowed; | |
| box-shadow: none; | |
| transform: none; | |
| } | |
| .loading-card .btn-primary[disabled]:hover, | |
| .loading-card .btn-primary[disabled]:active { | |
| transform: none; | |
| box-shadow: none; | |
| } | |
| /* Honest state on the ring: it spins only while work is genuinely in flight. The shipped | |
| build left `animation: spin` running unconditionally, so the card read | |
| "8th Wall engine loaded" underneath a still-turning spinner. */ | |
| .loading-ring.is-ready { | |
| animation: none; | |
| border-color: var(--success, #32D74B); | |
| box-shadow: 0 0 22px rgba(50, 215, 75, 0.28); | |
| } | |
| .loading-ring.is-ready::after { | |
| content: ''; | |
| display: block; | |
| width: 26px; | |
| height: 14px; | |
| margin: 34px auto 0; | |
| border-left: 3px solid var(--success, #32D74B); | |
| border-bottom: 3px solid var(--success, #32D74B); | |
| transform: rotate(-45deg); | |
| } | |
| .loading-ring.is-error { | |
| animation: none; | |
| border-color: var(--destructive, #FF3B30); | |
| box-shadow: 0 0 22px rgba(255, 84, 112, 0.24); | |
| } | |
| .loading-card .btn-primary:hover, | |
| .loading-card .btn-primary:focus-visible { | |
| transform: translateY(-1px); | |
| box-shadow: 0 8px 24px rgba(0, 212, 255, 0.28); | |
| } | |
| .loading-card .btn-primary:active { | |
| transform: translateY(0) scale(0.985); | |
| } | |
| /* Camera consent, stated before the browser prompt fires. Left-aligned deliberately: it is | |
| the one block on this card that is read as a sentence rather than scanned as a label. */ | |
| .gate-consent { | |
| display: flex; | |
| gap: 9px; | |
| margin: 0 0 14px; | |
| padding: 10px 11px; | |
| text-align: left; | |
| border-radius: 12px; | |
| background: rgba(0, 212, 255, 0.10); | |
| border: 1px solid rgba(0, 212, 255, 0.20); | |
| } | |
| .gate-consent-icon { | |
| flex: 0 0 auto; | |
| width: 17px; | |
| height: 17px; | |
| margin-top: 1px; | |
| color: var(--accent); | |
| } | |
| .loading-card .gate-consent p { | |
| margin: 0; | |
| font-size: 11.5px; | |
| line-height: 1.45; | |
| color: rgba(255, 255, 255, 0.80); | |
| } | |
| .loading-card .gate-foot { | |
| margin: 10px 0 0; | |
| font-size: 10.5px; | |
| color: var(--text-tertiary); | |
| } | |
| /* ---- Status panel -------------------------------------------------------- */ | |
| .status-panel { | |
| position: relative; | |
| transition: border-color 200ms ease; | |
| } | |
| .status-panel::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; left: 0; | |
| width: 12px; height: 12px; | |
| border-top: 2px solid var(--omega-accent, #00d4ff); | |
| border-left: 2px solid var(--omega-accent, #00d4ff); | |
| opacity: 0.5; | |
| pointer-events: none; | |
| } | |
| /* The label must stay quieter than the value, but it still has to be readable. It was | |
| --text-tertiary (already 0.85 alpha) with a further opacity: 0.65 on top, compounding to | |
| roughly 0.55 of a mid-grey — about 1.4:1 against the HUD, i.e. invisible on a phone in | |
| daylight. Setting the colour once, explicitly, instead of stacking two transparencies: | |
| this composites to ~7.7:1 while still reading as secondary next to the full-white value. */ | |
| .status-panel span { | |
| letter-spacing: 0.14em; | |
| font-size: 10px; | |
| text-transform: uppercase; | |
| color: rgba(255, 255, 255, 0.62); | |
| } | |
| /* Values are the live signal — they get the weight, the labels stay quiet. */ | |
| .status-panel strong { | |
| font-variant-numeric: tabular-nums; | |
| letter-spacing: 0.04em; | |
| transition: color 200ms ease; | |
| } | |
| /* State colouring is driven by the value the AR runtime actually writes, not by | |
| a timer — a HUD that shows "tracking" while tracking is dead is the exact | |
| dishonest-state failure the frontend skill names. Colour is always paired | |
| with the word itself, never carrying the meaning alone (accessibility). */ | |
| .status-panel strong[data-state="ok"] { color: #48e08a; } | |
| .status-panel strong[data-state="warn"] { color: #ffb800; } | |
| .status-panel strong[data-state="error"] { color: #ff4d4f; } | |
| .status-panel strong[data-state="idle"] { color: rgba(255, 255, 255, 0.55); } | |
| /* A live tick that breathes only while a state is genuinely active. */ | |
| .status-panel strong[data-state="ok"]::after { | |
| content: ''; | |
| display: inline-block; | |
| width: 5px; | |
| height: 5px; | |
| margin-left: 7px; | |
| border-radius: 50%; | |
| background: currentColor; | |
| vertical-align: middle; | |
| animation: omega-status-pulse 2.2s ease-in-out infinite; | |
| } | |
| @keyframes omega-status-pulse { | |
| 0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(72, 224, 138, 0.5); } | |
| 50% { opacity: 0.5; box-shadow: 0 0 0 5px transparent; } | |
| } | |
| @media (prefers-reduced-motion: reduce) { | |
| .loading-card { animation: none; } | |
| .loading-card h1::after { animation: none; width: 58px; } | |
| .loading-card .btn-primary { transition: none; } | |
| .status-panel strong[data-state="ok"]::after { animation: none; } | |
| } | |
| /* Part 40 Claude Design pass: webar/status-panel.html. | |
| hud.css already carried a data-state severity system (ok/warn/error/idle) whose own | |
| comment claimed the AR runtime drove it. Nothing ever set the attribute, so it was dead | |
| CSS and the panel always rendered flat accent. setStatus() now sets it. These rules add | |
| the signals that system lacked, so severity never rests on colour alone: a dot whose | |
| SHAPE differs per state, and weight on a fault. */ | |
| .status-panel strong { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .status-panel strong::before { | |
| content: ''; | |
| width: 7px; | |
| height: 7px; | |
| border-radius: 50%; | |
| flex: none; | |
| background: currentColor; | |
| } | |
| /* The colours are restated here, in the last block, because the base | |
| `.status-panel strong { color: var(--accent) }` rule was winning the cascade in the real | |
| page — measured in headless Chrome, not assumed: every severity computed to | |
| rgb(53,230,255) while weight and dot shape were already responding correctly. */ | |
| .status-panel strong[data-state="ok"] { color: #48e08a; } | |
| .status-panel strong[data-state="warn"] { color: #ffb800; } | |
| .status-panel strong[data-state="error"] { color: #ff4d4f; } | |
| .status-panel strong[data-state="idle"] { color: rgba(255, 255, 255, 0.55); } | |
| /* warn: a HOLLOW ring, breathing slowly — visibly different in shape from ok/error */ | |
| .status-panel strong[data-state="warn"]::before { | |
| background: transparent; | |
| box-shadow: inset 0 0 0 1.5px currentColor; | |
| animation: status-breathe 2.2s ease-in-out infinite; | |
| } | |
| /* error: filled dot with a halo, and the value goes bold */ | |
| .status-panel strong[data-state="error"] { | |
| font-weight: 700; | |
| } | |
| .status-panel strong[data-state="error"]::before { | |
| box-shadow: 0 0 0 3px rgba(255, 77, 79, 0.24); | |
| } | |
| @keyframes status-breathe { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } | |
| @media (prefers-reduced-motion: reduce) { | |
| .status-panel strong[data-state="warn"]::before { animation: none; } | |
| } | |