.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 !important; } .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; } }