@import 'tailwindcss';
@import './neon-frame.css';
:root { font-family: Arial, Helvetica, sans-serif; color: #deded7; background: #10110f; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; }
* { box-sizing: border-box; }
body { margin: 0; }
button, input, a { -webkit-tap-highlight-color: transparent; }
button, input { font: inherit; }
button { cursor: pointer; }
button:focus-visible, a:focus-visible { outline: 2px solid #d7e1d1; outline-offset: 5px; }
/* ── V.AI STUDIO background ────────────────────────────────────────────────
* The same artwork vai-avatar shows on its body: public/avatars/vaistudio_center.png,
* copied here byte-for-byte so the two Spaces read as one brand (md5 checked).
*
* Rendered at 30% opacity as a separate fixed layer rather than as a
* `background-image` on .app, because:
*
* * opacity on the element would fade the photographs and the search pill
* along with it, and
* * `background-attachment: fixed` + `background-image` cannot be composed
* with the two dimming layers (.vignette / .grain) that already sit on top
* of the field, so the artwork would fight them instead of sitting under.
*
* `pointer-events: none` keeps it from swallowing clicks meant for a tile, and
* z-index 0 puts it below .image-field (z-index auto, painted later) while
* still above the base background colour.
*
* The image is 816x816 and deliberately centred-cropped on the logo, matching
* vai-avatar's own mobile rule: on narrow portrait screens it is sized to full
* width and tiled vertically so the artwork is never cropped to a logo sliver.
*/
.brand-backdrop {
position: fixed; inset: 0; z-index: 0; pointer-events: none;
background-color: #10110f;
background-image: url('/brand/vaistudio_center.png');
background-size: cover;
background-position: center center;
background-repeat: no-repeat;
background-attachment: fixed;
opacity: .3;
}
@media (max-width: 768px) {
.brand-backdrop { background-position: center top; background-size: 100% auto; background-repeat: repeat; background-attachment: scroll; }
}
.app { min-height: 320px; background: #11120f; overflow: clip; }
/* Above the backdrop, below the field's own dimming layers. */
.app { background: transparent; }
/* Unlike hidden, clip prevents focus/scrollIntoView from panning the canvas. */
.image-field { position: absolute; inset: 0; overflow: clip; contain: strict; }
.image-tile { position: absolute; left: 0; top: 0; display: block; border: 0; padding: 0; border-radius: 2px; background-color: #252823; background-size: cover; overflow: hidden; will-change: transform; transform-origin: center; box-shadow: 0 1px 5px #0002; }
/* position:absolute so the share anchor's z-index:1 actually stacks above the
photograph. A z-index on a static element is ignored, and without this the
-- appended after the anchor when the thumbnail loads -- paints on top
of it, so the share button opens the viewer instead of sharing. */
.image-tile img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0; filter: brightness(.92); transition: opacity 120ms ease, filter 120ms ease; }
.image-tile img.loaded { opacity: 1; }
.image-tile:hover { box-shadow: 0 10px 35px #0005; }
.image-tile:hover img { filter: brightness(1); }
/* z-index:1 is load-bearing. The tile holds two children -- the share anchor
and, once it loads, an
at width/height 100%. The image is appended
later, so without this it paints on top and every click lands on the
photograph: the share button opens the viewer it was meant to bypass. */
/* Share link on the tile. Hidden until hover so the field stays a field: 150
photographs each wearing a button turns the plane into a control panel. It is
aria-hidden and tabIndex -1 (see ImageField), so this is purely a pointer
affordance -- the reachable share is the named link inside the viewer.
Sized in the tile's own coordinate space, so it scales with the photo. */
.image-share { position: absolute; z-index: 1; right: 5px; top: 5px; display: grid; place-items: center; width: 22px; height: 22px; border-radius: 6px; background: #0d0f0bb8; -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); color: #e6ebe0; opacity: 0; transform: scale(.82); transition: opacity 160ms ease, transform 160ms ease, background 160ms ease, color 160ms ease; }
.image-share svg { width: 13px; height: 13px; }
.image-share:hover { background: #0d0f0be6; color: #ffffff; }
.image-share[hidden] { display: none; }
.image-tile:hover .image-share, .image-tile:focus-within .image-share { opacity: 1; transform: scale(1); }
/* Top-ranked results are the ones most likely to be sent, so theirs show the
button without waiting for hover. */
.image-tile[data-rank="1"] .image-share, .image-tile[data-rank="2"] .image-share, .image-tile[data-rank="3"] .image-share { opacity: 1; transform: scale(1); }
@media (prefers-reduced-motion: reduce) { .image-share { transition: opacity 120ms linear; } }
/* Relevance is a soft per-photo treatment, not a second results layer.
* neon-frame.css owns the one exception -- a ringed tile is never dimmed --
* because that is a property of the ring, not of the fade. */
.image-tile { opacity: var(--relevance-opacity, 1); filter: blur(var(--relevance-blur, 0px)); transition: opacity 650ms ease, filter 650ms ease; }
.image-tile:hover, .image-tile:focus-visible { opacity: 1; filter: blur(0); }
/*
Lift the hovered tile above its neighbours.
Required for the share button to be usable at all. Tiles overlap by design and
each tile's own z-index is a stable seed of its id (ImageField.jsx sets 1..100
from a hash), not its search rank -- so a tile stays buried under whichever
neighbour hashed higher, permanently. Measured on the idle field: of 79
in-viewport share buttons, 0 were the topmost element at their own centre --
13 were covered by another tile's photograph, 8 by another tile outright, and
the rest hit their own icon. There is no way to express "raise me above my
neighbours" for the button alone, because the button cannot be hovered until
it is on top. Scoped to .image-tile:hover, which also covers :focus-within, so
keyboard users reach the same affordance.
*/
.image-tile:hover, .image-tile:focus-within { z-index: 2147483647; }
.vignette { position: absolute; inset: 0; pointer-events: none; z-index: 300; background: radial-gradient(ellipse at center, transparent 42%, #0c0d0b30 100%), linear-gradient(0deg, #10110f99, transparent 70px); }
.grain { position: absolute; inset: 0; z-index: 301; pointer-events: none; opacity: .045; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.92' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Cpath fill='%23fff' filter='url(%23n)' opacity='.7' d='M0 0h180v180H0z'/%3E%3C/svg%3E"); }
.search-center { position: absolute; z-index: 400; width: clamp(350px, 36vw, 640px); max-width: calc(100% - 44px); left: 50%; top: 50%; transform: translate(-50%, -50%); }
.search-pill { position: relative; height: clamp(62px, 4.8vw, 84px); border-radius: 999px; background: #e4e8ddbb; -webkit-backdrop-filter: blur(26px) saturate(.7); backdrop-filter: blur(26px) saturate(.7); box-shadow: inset 0 1px 0 #ffffff3d, 0 6px 36px #00000018; overflow: hidden; transition: background 250ms, box-shadow 250ms; }
.search-pill:focus-within { background: #e6eaddd4; box-shadow: inset 0 1px 0 #ffffff52, 0 5px 40px #0002; }
.search-pill input { width: 100%; height: 100%; background: transparent; color: #20251f; border: 0; outline: none; padding: 0 58px 0 27px; font-size: clamp(22px, 1.8vw, 30px); letter-spacing: -.035em; caret-color: #323b2d; }
.search-pill input::placeholder { color: #565d54; opacity: 1; }
.search-pill input::selection { background: #80957070; }
.clear-button { position: absolute; right: 13px; top: 50%; transform: translateY(-50%); width: 34px; height: 34px; display: grid; place-items: center; padding: 7px; color: #4c5449; border: 0; background: transparent; border-radius: 50%; transition: background 150ms; }
.clear-button:hover { background: #0000000d; }
.clear-button svg { width: 18px; height: 18px; }
/* ── The neon frame lives in src/neon-frame.css ─────────────────────────────
* Imported at the top of this file rather than kept inline, for one reason: it
* also has to ship as dist/assets/neon-frame.css, which index.html loads after
* the built app.css.
*
* The Space is `sdk: static`, so dist/ -- not src/ -- is what the browser gets.
* Until the bundle is rebuilt from source, that built app.css still carries the
* old `@keyframes neon-run { transform: rotate(1turn) }`, which rotated the
* ring's mask along with its gradient and pulled the painted border off the
* tile's edges. The override file is the fix that does not need a rebuild; this
* import is what stops the next rebuild from dropping it.
*
* One definition, two delivery routes. They are kept in step deliberately: the
* override is inert once the build includes these rules, rather than the two
* quietly describing different effects.
*/
/* ── Search status, footer, and the full-screen viewer ────────────────────
* Restored wholesale. The previous edit removed this whole tail by accident
* while stripping the size/position controls, which left every one of these
* classes unstyled: opening a photo rendered a raw