/* Simple console: page chrome (navbar, pills, buttons, segments, setting rows
   and toggles) comes from style.css; this file only owns the operate stage,
   the floating controls and the settings-page extras that have no
   advanced-console counterpart. */
.symbols{position:absolute;width:0;height:0}

/* ---------- Transient toast ---------- */
/* Semantic info / negative message: flat tinted face, inset ring, no border.
   A drag tracks the finger while lifting the toast; past the threshold it
   hands off to the same `.leaving` transition the close button and the
   auto-hide timer use, so all three exits look identical. */
#message{position:fixed;top:calc(var(--nav-height) + 12px + env(safe-area-inset-top));left:50%;transform:translate(-50%,0);z-index:100;width:max-content;max-width:min(90vw,560px);display:flex;align-items:flex-start;gap:.6em;padding:1em 1.5em;background:var(--primary-tint);box-shadow:0 0 0 1px var(--primary-border) inset,var(--shadow-md);border-radius:var(--radius);color:#276f86;font-weight:700;transition:transform .18s ease,opacity .18s ease;touch-action:none}
#message.leaving{transform:translate(-50%,-28px);opacity:0}
#message.dragging{transition:none}
#message.error{background:var(--negative-tint);color:var(--negative-text);box-shadow:0 0 0 1px var(--negative-border) inset,var(--shadow-md)}
#message:focus-visible{outline:2px solid var(--focus-border);outline-offset:3px}
#message-text{flex:1;padding-top:.05em}
/* A toast can offer what to do next -- after a photo, download or view it. */
#message-actions{flex:none;display:flex;gap:6px;margin:-.35em 0;align-self:center}
.message-action{display:inline-flex;align-items:center;min-height:30px;padding:0 12px;border:1px solid var(--primary-border);border-radius:var(--radius);background:#fff;color:inherit;font:inherit;font-size:13px;text-decoration:none;white-space:nowrap;cursor:pointer;transition:background-color .1s ease}
.message-action:hover{background:var(--primary-tint)}
.message-action.primary{border-color:var(--primary);background:var(--primary);color:#fff}
.message-action.primary:hover{background:var(--primary-hover)}
.message-action:focus-visible{outline:2px solid var(--focus-border);outline-offset:2px}
#message-close{flex:none;margin:-.3em -.6em -.3em 0;border:0;background:none;color:inherit;opacity:.55;font-size:20px;line-height:1;padding:.3em .6em;cursor:pointer;transition:opacity .1s ease}
#message-close:hover{opacity:1}
#message-close:focus-visible{outline:2px solid var(--focus-border);outline-offset:2px;opacity:1}

/* ---------- Operate stage ---------- */
/* The operate page never scrolls: the body is pinned to the viewport and the
   stage takes whatever height the navbar and the page padding leave, so no
   padding or bar height has to be repeated in a calc() that can drift from
   them. The height is the viewport simple.js measures (--app-height), with
   dvh before the first reading and vh for browsers without dvh. */
body.operating{height:100vh;height:var(--app-height,100dvh);min-height:0;overflow:hidden;display:flex;flex-direction:column}
body.operating main{flex:1;min-height:0;width:100%;display:flex;flex-direction:column;padding-bottom:20px}
body.operating #operate{flex:1;min-height:0;display:flex;flex-direction:column}
#stage{position:relative;flex:1;min-height:0;max-height:1000px;background:#1b1c1d;border-radius:var(--radius);overflow:hidden;isolation:isolate;touch-action:none;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none}
#stage video{display:block;width:100%;height:100%;object-fit:contain;pointer-events:none}
#video-status{position:absolute;top:12px;left:12px;color:#ffffffa6;font-size:12px;pointer-events:none}
#stage-toolbar{position:absolute;right:54px;top:10px;z-index:2;display:flex;align-items:center;gap:10px}
#viewer-count{min-height:34px;display:flex;align-items:center;color:rgba(255,255,255,.75);font-size:12px;font-weight:600;pointer-events:none}
#focus-auto{border:0;border-radius:var(--radius);background:rgba(0,0,0,.6);box-shadow:0 0 0 1px rgba(255,255,255,.2) inset;color:rgba(255,255,255,.8);padding:4px 9px;font-size:12px;font-weight:700;min-height:34px;cursor:pointer;transition:background-color .1s ease,color .1s ease}
#focus-auto:hover{background:var(--primary);box-shadow:none;color:#fff}
#focus-auto{background:rgba(251,189,8,.85);box-shadow:none;color:#1b1c1d}
#focus-auto:focus-visible{outline:2px solid var(--focus-border);outline-offset:3px}
/* Sound switches: the icon itself says on or off, and only the "on" state is
   coloured. Hover never borrows that colour, and only applies where a pointer
   can actually hover -- on a phone :hover sticks to the button just tapped,
   which made a switch turned off look as if it were still on. */
.stage-toggle{width:34px;height:34px;padding:0;display:grid;place-items:center;border:0;border-radius:var(--radius);background:rgba(0,0,0,.6);box-shadow:0 0 0 1px rgba(255,255,255,.2) inset;color:rgba(255,255,255,.8);cursor:pointer;transition:background-color .1s ease,color .1s ease}
.stage-toggle i.icon{display:none;margin:0;width:auto;height:auto;font-size:16px;line-height:1;pointer-events:none}
.semantic-icons-ready .stage-toggle i.icon{display:block}
.semantic-icons-ready .stage-toggle .icon-fallback{display:none}
.stage-toggle svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;pointer-events:none}
#listen[aria-pressed="true"]{background:var(--primary);box-shadow:none;color:#fff}
#talk[aria-pressed="true"]{background:#db2828;box-shadow:none;color:#fff}
#actions-toggle[aria-expanded="true"]{background:var(--primary);box-shadow:none;color:#fff}
@media(hover:hover){
  .stage-toggle:not(:disabled):hover{background:rgba(0,0,0,.8);color:#fff}
  #listen[aria-pressed="true"]:not(:disabled):hover{background:var(--primary-hover)}
  #talk[aria-pressed="true"]:not(:disabled):hover{background:#c21f1f}
  #actions-toggle[aria-expanded="true"]:hover{background:var(--primary-hover)}
}
.stage-toggle:disabled{opacity:.45;cursor:not-allowed}
.stage-toggle:focus-visible{outline:2px solid var(--focus-border);outline-offset:3px}
body.immersive #stage-toolbar{top:calc(10px + env(safe-area-inset-top));right:calc(54px + env(safe-area-inset-right))}

/* ---------- Tap-to-focus reticle ---------- */
/* Centred on the double-tapped point (left/top set from JS). The colour is the
   camera's own verdict, carried on every frame as AfState: amber while the
   lens is scanning, green once it reports focus, red when it could not find
   one. The ring is not on a timer -- it holds until that first verdict
   arrives, then fades. It says nothing about what happens after: the camera
   goes on tracking that point, and the toolbar button is what shows it. */
#focus-ring{position:absolute;z-index:2;width:84px;height:84px;margin:-42px 0 0 -42px;border:2px solid rgba(251,189,8,.95);border-radius:6px;box-shadow:0 0 0 1px rgba(0,0,0,.35),0 0 0 1px rgba(0,0,0,.35) inset;pointer-events:none;opacity:1;transition:border-color .15s ease,opacity .35s ease,transform .2s ease;animation:focus-land .25s ease-out}
#focus-ring.scanning{animation:focus-land .25s ease-out,focus-pulse 1s ease-in-out .25s infinite}
#focus-ring.focused{border-color:rgba(33,186,69,.95);transform:scale(.88)}
#focus-ring.failed{border-color:rgba(219,40,40,.95)}
#focus-ring.leaving{opacity:0}
@keyframes focus-land{from{transform:scale(1.5);opacity:0}to{transform:scale(1);opacity:1}}
@keyframes focus-pulse{50%{opacity:.5}}
#stage.shutter:after{content:"";position:absolute;inset:0;z-index:3;background:#fff;pointer-events:none;animation:shutter-flash .35s ease-out forwards}
@keyframes shutter-flash{from{opacity:.6}to{opacity:0}}
@media(prefers-reduced-motion:reduce){#focus-ring,#focus-ring.scanning{animation:none}#focus-ring.focused{transform:none}}

/* ---------- Collapsible quick-actions dock ---------- */
.actions{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:2;display:flex;flex-direction:row;gap:18px}
/* Semantic circular buttons floating over the video: flat white face, bold
   icon, Semantic's grey hover and the blue/red states from its colour set. */
.round{width:64px;height:64px;border:0;border-radius:50%;padding:18px;background:#fff;color:var(--text-muted);box-shadow:0 1px 2px 0 rgba(34,36,38,.15),0 2px 10px 0 rgba(0,0,0,.3);position:relative;cursor:pointer;transition:background-color .1s ease,color .1s ease}
.round:hover:not(:disabled){background:#f2f3f4;color:var(--text)}
.round:disabled{opacity:.45;cursor:not-allowed}
.round:focus-visible{outline:2px solid var(--focus-border);outline-offset:3px}
.round i.icon{display:none;margin:0;width:100%;height:100%;font-size:28px;line-height:1;align-items:center;justify-content:center;pointer-events:none}
.semantic-icons-ready .round i.icon{display:flex}
.semantic-icons-ready .round .icon-fallback{display:none}
.round svg{width:100%;height:100%;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
#light[aria-pressed=true]{background:#fbbd08;color:#fff}
#light[aria-pressed=true]:hover{background:#eaae00}
/* While a photo is on its way the shutter spins a ring like a running task's,
   in the accent colour since there is nothing to stop. It stays pressable:
   taps in a row are a burst. */
#snapshot.capturing:before{content:"";position:absolute;inset:-6px;border:3px solid rgba(255,255,255,.35);border-top-color:var(--primary);border-radius:50%;animation:spin 1.3s linear infinite}
/* With live photos on, the shutter wears the live mark, as a phone camera's does. */
#snapshot .shutter-live{display:none;position:absolute;top:-2px;right:-2px;width:24px;height:24px;padding:3px;border-radius:50%;background:#fff;color:var(--text);box-shadow:0 1px 3px rgba(0,0,0,.3);stroke-width:1.5}
.live-photo-on #snapshot .shutter-live{display:block}
/* Holding the shutter fills a red ring around it; full, recording starts. The
   ring's .6s is RECORD_HOLD_MS in simple.js. Recording, the shutter becomes
   the red stop button. */
#snapshot .shutter-hold{display:none;position:absolute;inset:-6px;width:calc(100% + 12px);height:calc(100% + 12px);transform:rotate(-90deg);pointer-events:none}
#snapshot.arming .shutter-hold{display:block}
#snapshot .shutter-hold circle{fill:none;stroke:var(--negative);stroke-width:3;stroke-linecap:round;stroke-dasharray:100;stroke-dashoffset:100;animation:shutter-hold .6s linear forwards}
@keyframes shutter-hold{to{stroke-dashoffset:0}}
#snapshot.recording,#snapshot.recording:hover{background:var(--negative);color:#fff}
#snapshot.recording>:is(i.icon,.icon-fallback,.shutter-live){visibility:hidden}
#snapshot.recording:after{content:"";position:absolute;left:50%;top:50%;width:34%;height:34%;transform:translate(-50%,-50%);border-radius:4px;background:#fff}
#snapshot.recording[aria-busy=true]:after{opacity:.5}
/* The stage's REC badge keeps a recording in sight with the menu folded away,
   and stops it when tapped. */
#record-indicator{min-height:34px;display:flex;align-items:center;gap:7px;padding:4px 11px;border:0;border-radius:var(--radius);background:rgba(219,40,40,.9);color:#fff;font:inherit;font-size:12px;font-weight:700;font-variant-numeric:tabular-nums;cursor:pointer}
#record-indicator[hidden]{display:none}
#record-indicator:focus-visible{outline:2px solid var(--focus-border);outline-offset:3px}
#record-indicator .rec-dot{width:8px;height:8px;border-radius:50%;background:#fff;animation:rec-blink 1s step-end infinite}
#record-indicator[aria-busy=true]{cursor:progress}
#record-indicator[aria-busy=true] .rec-dot{animation:none;opacity:.5}
@keyframes rec-blink{50%{opacity:0}}
@media(prefers-reduced-motion:reduce){#record-indicator .rec-dot{animation:none}}
/* Semantic compact selection dropdown: hairline box with a caret; opening it
   turns the border Semantic's focus blue and attaches the menu flush beneath
   (or above, .upward, when the page has no room below), sliding it in the way
   Semantic's default "slide down" transition does. The bold item is the
   current value; the grey one is where the pointer or the arrow keys are. */
.selection-dropdown{position:relative;flex:none}
.selection-dropdown>button{position:relative;display:block;min-width:8.5em;min-height:2.71428571em;margin:0;padding:.78571429em 2.1em .78571429em 1em;background:#fff;border:1px solid rgba(34,36,38,.15);border-radius:.28571429rem;box-shadow:none;color:rgba(0,0,0,.87);font:inherit;line-height:1em;text-align:left;white-space:nowrap;cursor:pointer;outline:0;transition:box-shadow .1s ease,border-color .1s ease}
.selection-dropdown>button:hover{border-color:rgba(34,36,38,.35)}
.selection-dropdown>button:focus-visible{border-color:#96c8da}
.selection-dropdown>button:disabled{opacity:.45;cursor:default;border-color:rgba(34,36,38,.15)}
.selection-dropdown.visible>button{border-color:#96c8da;box-shadow:0 2px 3px 0 rgba(34,36,38,.15);border-bottom-left-radius:0;border-bottom-right-radius:0}
.selection-dropdown.visible.upward>button{box-shadow:0 0 3px 0 rgba(0,0,0,.08);border-radius:0 0 .28571429rem .28571429rem}
.selection-dropdown>button>i.icon{display:none;position:absolute;top:50%;right:1em;margin:-.5em 0 0;width:auto;height:1em;line-height:1;opacity:.8}
.selection-dropdown>button>.icon-fallback{position:absolute;top:50%;right:.8em;width:1.1em;height:1.1em;margin-top:-.55em;opacity:.8}
.semantic-icons-ready .selection-dropdown>button>i.icon{display:block}
.semantic-icons-ready .selection-dropdown>button>.icon-fallback{display:none}
.selection-dropdown>.menu{position:absolute;top:100%;left:0;z-index:11;width:calc(100% + 2px);min-width:calc(100% + 2px);margin:0 -1px;padding:0;list-style:none;background:#fff;border:1px solid #96c8da;border-top-width:0;border-radius:0 0 .28571429rem .28571429rem;box-shadow:0 2px 3px 0 rgba(34,36,38,.15);overflow:hidden;transform-origin:top center;animation:selection-slide-down .2s ease}
.selection-dropdown.upward>.menu{top:auto;bottom:100%;border-top-width:1px;border-bottom-width:0;border-radius:.28571429rem .28571429rem 0 0;box-shadow:0 -2px 3px 0 rgba(0,0,0,.08);transform-origin:bottom center}
.selection-dropdown>.menu>li{padding:.78571429rem 1.14285714rem;border-top:1px solid #fafafa;color:rgba(0,0,0,.87);line-height:1em;white-space:nowrap;cursor:pointer;user-select:none;-webkit-user-select:none}
.selection-dropdown>.menu>li:first-child{border-top:none}
.selection-dropdown>.menu>li.active{font-weight:700;color:rgba(0,0,0,.95)}
.selection-dropdown>.menu>li:hover,.selection-dropdown>.menu>li.selected{background:rgba(0,0,0,.05);color:rgba(0,0,0,.95)}
@keyframes selection-slide-down{from{opacity:0;transform:scaleY(0)}to{opacity:1;transform:scaleY(1)}}
@media (prefers-reduced-motion:reduce){.selection-dropdown>.menu{animation:none}}
#light[aria-busy=true]{box-shadow:0 0 0 2px var(--focus-border),0 2px 10px 0 rgba(0,0,0,.3)}
.round.running{color:var(--negative)}
.round.succeeded{color:var(--positive)}
.round.failed{color:var(--negative)}
.round.running:before{content:"";position:absolute;inset:-6px;border:3px solid rgba(255,255,255,.35);border-top-color:var(--negative);border-radius:50%;animation:spin 1.3s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}

/* ---------- Touch joysticks ---------- */
/* Hidden until a touch presses a control zone of the stage, then shown centered
   on that touch point (left/top are set from JS) and faded out on release. The
   pads are display-only: input is tracked on #stage, so the finger may drift
   anywhere after engagement. A landscape stage summons the two linear pads from
   its outer thirds; a portrait stage summons the round omni pad from its bottom
   third so one thumb drives both axes. Arrows are CSS triangles, not text: a
   long-press on text arrows would trigger the browser's copy UI. */
.pad{position:absolute;z-index:2;display:flex;align-items:center;justify-content:space-between;padding:7px;background:rgba(255,255,255,.09);border:1px solid rgba(255,255,255,.18);border-radius:999px;pointer-events:none;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none;opacity:0;visibility:hidden;transition:opacity .25s ease,visibility .25s ease}
.pad.visible{opacity:1;visibility:visible;background:rgba(33,133,208,.35);border-color:rgba(101,185,246,.7)}
.pad-arrow{width:0;height:0;flex:none;border:7px solid transparent;pointer-events:none}
.pad-arrow-up{border-bottom-color:rgba(255,255,255,.55);border-top:0}
.pad-arrow-down{border-top-color:rgba(255,255,255,.55);border-bottom:0}
.pad-arrow-left{border-right-color:rgba(255,255,255,.55);border-left:0}
.pad-arrow-right{border-left-color:rgba(255,255,255,.55);border-right:0}
.pad-knob{width:34px;height:34px;flex:none;border-radius:50%;background:rgba(255,255,255,.7);box-shadow:0 2px 10px #0003;pointer-events:none}
.pad.vertical{width:52px;height:172px;flex-direction:column}
.pad.horizontal{width:172px;height:52px}
.pad.omni{width:176px;height:176px;display:block;padding:0}
.pad.omni .pad-arrow{position:absolute}
.pad.omni .pad-arrow-up{left:50%;top:10px;margin-left:-7px}
.pad.omni .pad-arrow-down{left:50%;bottom:10px;margin-left:-7px}
.pad.omni .pad-arrow-left{top:50%;left:10px;margin-top:-7px}
.pad.omni .pad-arrow-right{top:50%;right:10px;margin-top:-7px}
.pad.omni .pad-knob{position:absolute;left:50%;top:50%;margin:-17px 0 0 -17px}

/* ---------- Head angle gauge ---------- */
/* A slim scale on the stage's right edge, top = upper limit, bottom = lower
   limit, with a tick at 0°. It appears while the head moves -- from any input,
   and while homing -- then fades. Between one-second state polls the marker is
   dead-reckoned from the commanded rate; the transition smooths the 10 Hz steps. */
#head-gauge{position:absolute;z-index:1;right:calc(18px + env(safe-area-inset-right));top:50%;height:min(44%,240px);transform:translateY(-50%);pointer-events:none;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease}
#head-gauge.visible{opacity:1;visibility:visible;transition-duration:.12s}
#head-gauge-value{position:absolute;right:18px;transform:translateY(-50%);transition:top .1s linear;white-space:nowrap;min-width:3.4em;padding:3px 6px;border-radius:var(--radius);background:rgba(0,0,0,.55);color:#fff;font-size:13px;font-variant-numeric:tabular-nums;text-align:right}
.head-gauge-track{position:relative;width:6px;height:100%;border-radius:3px;background:rgba(255,255,255,.22);box-shadow:0 0 0 1px rgba(0,0,0,.3)}
.head-gauge-zero{position:absolute;left:-5px;right:-5px;height:2px;margin-top:-1px;background:rgba(255,255,255,.6)}
.head-gauge-marker{position:absolute;left:50%;width:16px;height:16px;margin:-8px 0 0 -8px;border-radius:50%;background:#2185d0;border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,.4);transition:top .1s linear}

@media(prefers-reduced-motion:reduce){.head-gauge-marker,#head-gauge-value{transition:none}}

/* ---------- Settings page ---------- */
#settings :is(#speed-value,#led-brightness-value,#video-bitrate-value){color:var(--primary);font-variant-numeric:tabular-nums}

.map-view{position:relative;width:100%;height:52dvh;min-height:230px;overflow:hidden;background:#f4f6f8;border:1px solid var(--border);border-radius:var(--radius)}
.map-view canvas{display:block;width:100%;height:100%;touch-action:none;-webkit-tap-highlight-color:transparent;user-select:none;-webkit-user-select:none}
.map-note{position:absolute;inset:0;margin:0;display:flex;align-items:center;justify-content:center;padding:16px;text-align:center;color:var(--text-muted);font-size:14px;pointer-events:none}
.map-note[hidden]{display:none}
.map-tools{display:flex;gap:8px;margin-top:12px}

/* ---------- System panel ---------- */
.system-actions{display:flex;flex-wrap:wrap;gap:8px}
.log-view{margin:14px 0 0;padding:12px;max-height:46dvh;overflow:auto;background:#f9fafb;border:1px solid var(--border);border-radius:var(--radius);color:var(--text-muted);font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;line-height:1.5;white-space:pre-wrap;overflow-wrap:anywhere;-webkit-overflow-scrolling:touch}
.log-view:focus-visible{outline:2px solid var(--focus-border);outline-offset:2px}

/* ---------- Album ---------- */
/* Thumbnails grouped by day under headers that stick below the navbar,
   newest first. A tile holds a grey shimmer until its thumbnail arrives and
   then fades it in. In selection mode every tile shows a check circle that
   fills in once it is picked, the picked photo shrinks inside a blue frame,
   and the batch actions ride in a bar pinned to the bottom of the screen,
   within reach of a thumb. */
.album-segment{padding-bottom:1em}
.album-segment .segment-header{margin-bottom:10px}
.album-actions{flex-wrap:wrap;justify-content:flex-end}
.album-filters{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 6px}
.album-filter{min-height:30px;padding:0 12px;border:0;border-radius:999px;background:#f3f4f5;box-shadow:0 0 0 1px var(--border) inset;color:var(--text-muted);font:inherit;font-size:13px;font-weight:600;font-variant-numeric:tabular-nums;cursor:pointer;transition:background-color .1s ease,color .1s ease}
.album-filter:hover{background:#e8e9ea;color:var(--text)}
.album-filter[aria-pressed=true]{background:var(--primary);box-shadow:none;color:#fff}
.album-filter:focus-visible{outline:2px solid var(--focus-border);outline-offset:2px}
.album-empty{display:flex;flex-direction:column;align-items:center;gap:10px;padding:3.5em 1em 3em;text-align:center;color:var(--text-muted)}
.album-empty-icon{width:52px;height:52px;fill:none;stroke:var(--text-faint);stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round}
.album-empty-title{margin:0;color:var(--text);font-size:16px;font-weight:700}
.album-empty-text{max-width:26em;margin:0;line-height:1.6}
.album-empty .btn{margin-top:6px}
.album-empty.loading .album-empty-icon{animation:album-breathe 1.2s ease-in-out infinite}
.album-empty.loading .album-empty-title{color:var(--text-muted);font-weight:600}
@keyframes album-breathe{50%{opacity:.35}}
.album-day + .album-day{margin-top:6px}
.album-day-header{position:sticky;top:var(--nav-height);z-index:3;display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:44px;margin:0 -1em;padding:6px 1em;background:rgba(255,255,255,.94);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
.album-day-title{margin:0;font-size:14px;font-weight:700;color:var(--text)}
.album-day-title .meta-text{margin-left:8px;font-weight:600}
.album-day-select{flex:none;min-height:30px;padding:0 10px;border:0;border-radius:var(--radius);background:none;color:var(--primary);font:inherit;font-size:13px;font-weight:700;cursor:pointer}
.album-day-select:hover{background:var(--primary-tint);box-shadow:0 0 0 1px var(--primary-border) inset}
.album-day-select:focus-visible{outline:2px solid var(--focus-border);outline-offset:2px}
.album-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(184px,1fr));gap:6px}
.album-item{position:relative;display:block;width:100%;aspect-ratio:16/9;padding:0;border:0;border-radius:var(--radius);overflow:hidden;background:linear-gradient(100deg,#ececed 30%,#f6f6f7 50%,#ececed 70%) 0 0/300% 100%;cursor:pointer;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none;animation:album-shimmer 1.4s linear infinite}
.album-item.loaded,.album-item.broken{animation:none;background:#e8e9ea}
.album-item.broken:before{content:"无法显示";position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--text-faint);font-size:12px}
@keyframes album-shimmer{to{background-position:-300% 0}}
.album-item img{display:block;width:100%;height:100%;object-fit:cover;pointer-events:none;opacity:0;transition:opacity .25s ease,transform .15s ease,border-radius .15s ease}
.album-item.loaded img{opacity:1}
.album-item.broken img{display:none}
.album-item:hover.loaded img{filter:brightness(1.06)}
.album-item:focus-visible{outline:2px solid var(--focus-border);outline-offset:2px}
.album-time{position:absolute;left:0;right:0;bottom:0;padding:16px 8px 5px;background:linear-gradient(transparent,rgba(0,0,0,.55));color:#fff;font-size:11px;font-weight:600;font-variant-numeric:tabular-nums;text-align:left;text-shadow:0 1px 2px rgba(0,0,0,.4);pointer-events:none}
.album-kind{position:absolute;left:6px;top:6px;padding:1px 6px;border-radius:3px;background:var(--positive);color:#fff;font-size:11px;font-weight:700;line-height:16px;box-shadow:0 1px 3px rgba(0,0,0,.25);pointer-events:none}
.album-live{position:absolute;left:5px;top:5px;width:20px;height:20px;color:#fff;filter:drop-shadow(0 1px 2px rgba(0,0,0,.55));pointer-events:none}
.album-live svg{display:block;width:100%;height:100%;fill:none;stroke:currentColor;stroke-width:2}
.album-live.pending{animation:album-breathe 1.4s ease-in-out infinite}
.album-duration{position:absolute;left:6px;top:6px;display:flex;align-items:center;gap:3px;padding:1px 6px 1px 3px;border-radius:3px;background:rgba(0,0,0,.55);color:#fff;font-size:11px;font-weight:700;line-height:16px;font-variant-numeric:tabular-nums;pointer-events:none}
.album-duration svg{width:14px;height:14px}
@media(prefers-reduced-motion:reduce){.album-live.pending{animation:none;opacity:.5}}
.album-check{position:absolute;top:6px;right:6px;width:22px;height:22px;border:2px solid #fff;border-radius:50%;background:rgba(0,0,0,.25);box-shadow:0 1px 3px rgba(0,0,0,.4);opacity:0;transform:scale(.6);transition:opacity .12s ease,transform .12s ease,background-color .12s ease;pointer-events:none}
.album-selecting .album-check{opacity:1;transform:none}
.album-item.selected{background:var(--primary-tint);box-shadow:0 0 0 3px var(--primary) inset}
.album-item.selected img{transform:scale(.86);border-radius:var(--radius)}
.album-item.selected .album-time{opacity:0}
.album-item.selected .album-check{background:var(--primary)}
.album-item.selected .album-check:after{content:"";position:absolute;left:6px;top:2px;width:5px;height:10px;border:solid #fff;border-width:0 2px 2px 0;transform:rotate(45deg)}
/* The selection bar floats over the page's bottom edge; the page grows by its
   height so the last row of photos can still scroll clear of it. */
body.album-selecting main{padding-bottom:calc(96px + env(safe-area-inset-bottom))}
.album-selectbar{position:fixed;left:50%;bottom:calc(16px + env(safe-area-inset-bottom));z-index:60;display:flex;align-items:center;gap:8px;width:min(calc(100% - 28px),520px);padding:8px 8px 8px 16px;transform:translateX(-50%);background:#fff;border-radius:calc(var(--radius) * 2);box-shadow:0 0 0 1px var(--border),0 6px 24px rgba(0,0,0,.18);animation:album-bar-in .18s ease-out}
.album-picked{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);font-weight:700;font-variant-numeric:tabular-nums}
.album-selectbar .btn:disabled{opacity:.45;cursor:default}
.album-selectbar[aria-busy=true] .btn{cursor:progress}
@keyframes album-bar-in{from{transform:translate(-50%,16px);opacity:0}}

/* ---------- Photo viewer ---------- */
/* The photo fills the whole screen; the bars float over it and a single tap
   hides them. The track moves for paging and the pull-down, the frame inside
   it for zoom, and both only animate when settling -- never while a finger
   is driving them. */
body.viewing{overflow:hidden}
.viewer{position:fixed;inset:0;z-index:200;overflow:hidden;background:#000;color:#fff;transition:background-color .2s ease}
.viewer-stage{position:absolute;inset:0;touch-action:none;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none;cursor:zoom-in}
.viewer.zoomed .viewer-stage{cursor:grab}
.viewer-track,.viewer-frame{position:absolute;inset:0;will-change:transform}
.viewer-track.animating{transition:transform .26s cubic-bezier(.2,.8,.2,1)}
.viewer-frame.animating{transition:transform .22s ease-out}
.viewer-frame img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;pointer-events:none}
.viewer-image{opacity:0;transition:opacity .2s ease}
/* The clip lies over the still at the same aspect and is shown only while it
   plays; ending, it dissolves back into the still. A held photo also lifts a
   little towards the finger, the way a phone's does. */
.viewer-frame video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;pointer-events:none;opacity:0;transition:opacity .3s ease}
.viewer.live-playing .viewer-live{opacity:1;transition-duration:.08s}
/* A recording plays in its own box, sized to the picture, so the native
   controls take the touches on it and the margins still page and close. */
.viewer-video{position:absolute;left:0;top:0;width:100%;height:100%;object-fit:contain;background:#000}
.viewer-video[hidden]{display:none}
.viewer.video .viewer-stage{cursor:default}
/* A cat photo's boxes, drawn over the untouched photo in its own pixels, so
   they letterbox and zoom with it; the stroke stays the same on screen. */
.viewer-boxes{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;opacity:0;transition:opacity .2s ease}
.viewer-boxes rect{fill:none;stroke:#ffc800;stroke-width:3;vector-effect:non-scaling-stroke;filter:drop-shadow(0 0 1.5px rgba(0,0,0,.6))}
.viewer.loaded:not(.boxes-hidden,.live-playing) .viewer-boxes{opacity:1}
.viewer-track{transition:scale .3s cubic-bezier(.2,.8,.2,1)}
.viewer.live-holding .viewer-track{scale:1.03}
/* The 实况 button: translucent and blurred over the photo, like a desktop
   photo viewer's, and lit while the clip plays. */
.viewer-live-badge{position:absolute;left:calc(12px + env(safe-area-inset-left));top:calc(64px + env(safe-area-inset-top));z-index:1;display:flex;align-items:center;gap:6px;height:34px;padding:0 14px 0 10px;border:1px solid rgba(255,255,255,.18);border-radius:var(--radius);background:rgba(32,32,32,.5);color:#fff;font:inherit;font-size:13px;font-weight:700;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,.25);backdrop-filter:blur(16px) saturate(1.4);-webkit-backdrop-filter:blur(16px) saturate(1.4);transition:opacity .2s ease,background-color .1s ease,border-color .1s ease}
.viewer-live-badge:hover{background:rgba(64,64,64,.6);border-color:rgba(255,255,255,.28)}
.viewer-live-badge:active{background:rgba(24,24,24,.6)}
.viewer-live-badge[aria-pressed=true]{background:rgba(255,255,255,.85);border-color:transparent;color:#111}
.viewer-live-badge:focus-visible{outline:2px solid var(--focus-border);outline-offset:2px}
.viewer-live-badge svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.8}
.viewer-live-badge .live-dot{fill:currentColor;stroke:none}
.viewer-live-badge .live-ring{stroke-dasharray:1.6 2.1;transform-origin:12px 12px}
.viewer.live-playing .viewer-live-badge .live-ring{animation:live-ring 1.6s linear infinite}
@keyframes live-ring{to{transform:rotate(360deg)}}
@media(prefers-reduced-motion:reduce){.viewer.live-holding .viewer-track{scale:none}.viewer.live-playing .viewer-live-badge .live-ring{animation:none}}
.viewer.loaded .viewer-image{opacity:1}
.viewer.loaded .viewer-placeholder{visibility:hidden;transition:visibility 0s .2s}
.viewer-spinner{position:absolute;left:50%;top:50%;width:36px;height:36px;margin:-18px 0 0 -18px;border:3px solid rgba(255,255,255,.25);border-top-color:#fff;border-radius:50%;animation:spin .9s linear infinite;pointer-events:none}
.viewer-error{position:absolute;left:50%;top:50%;margin:0;padding:8px 14px;transform:translate(-50%,-50%);border-radius:var(--radius);background:rgba(0,0,0,.6);color:rgba(255,255,255,.9);font-weight:600;pointer-events:none}
.viewer-bar{position:absolute;left:0;right:0;top:0;z-index:1;display:flex;align-items:center;gap:8px;padding:calc(10px + env(safe-area-inset-top)) calc(12px + env(safe-area-inset-right)) 28px calc(12px + env(safe-area-inset-left));background:linear-gradient(rgba(0,0,0,.6),rgba(0,0,0,.25) 60%,transparent);pointer-events:none}
.viewer-bar > *{pointer-events:auto}
.viewer-heading{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px;padding-left:2px}
.viewer-title,.viewer-meta{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-variant-numeric:tabular-nums;text-shadow:0 1px 2px rgba(0,0,0,.5)}
.viewer-title{font-size:14px;font-weight:700}
.viewer-meta{font-size:12px;color:rgba(255,255,255,.7)}
.viewer-btn{flex:none;min-height:36px;padding:5px 14px;border:0;border-radius:var(--radius);background:rgba(255,255,255,.14);color:#fff;font:inherit;font-size:13px;font-weight:700;line-height:26px;text-decoration:none;cursor:pointer;backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);transition:background-color .1s ease}
.viewer-btn:hover{background:rgba(255,255,255,.26)}
/* The 框 toggle is lit while the boxes show. */
.viewer-btn[aria-pressed=true]{background:rgba(255,255,255,.85);color:#111}
.viewer-btn:focus-visible,.viewer-nav:focus-visible{outline:2px solid var(--focus-border);outline-offset:2px}
.viewer-danger:hover{background:var(--negative)}
/* A live photo's 下载 is held for its menu, never the browser's link menu. */
.viewer-download{position:relative;flex:none;display:flex}
#viewer-download{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}
.viewer-menu{position:absolute;top:calc(100% + 6px);right:0;min-width:120px;display:flex;flex-direction:column;padding:4px;border-radius:var(--radius);background:rgba(30,30,30,.92);box-shadow:0 6px 20px rgba(0,0,0,.4);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)}
.viewer-menu[hidden]{display:none}
.viewer-menu a{padding:9px 12px;border-radius:calc(var(--radius) - 2px);color:#fff;font-size:13px;font-weight:700;text-decoration:none;white-space:nowrap}
.viewer-menu a:hover{background:rgba(255,255,255,.16)}
.viewer-menu a:focus-visible{outline:2px solid var(--focus-border);outline-offset:-2px}
.viewer-icon-btn{width:36px;padding:0;display:flex;align-items:center;justify-content:center}
.viewer-icon-btn svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round}
.viewer-nav{position:absolute;top:50%;z-index:1;width:48px;height:72px;margin-top:-36px;border:0;border-radius:var(--radius);background:rgba(0,0,0,.35);color:#fff;font-size:36px;line-height:1;cursor:pointer;transition:background-color .1s ease,opacity .2s ease}
.viewer-nav:hover:not(:disabled){background:rgba(255,255,255,.22)}
.viewer-nav:disabled{opacity:0;pointer-events:none}
.viewer-prev{left:calc(10px + env(safe-area-inset-left))}
.viewer-next{right:calc(10px + env(safe-area-inset-right))}
.viewer-index{position:absolute;left:50%;bottom:calc(16px + env(safe-area-inset-bottom));z-index:1;padding:3px 12px;transform:translateX(-50%);border-radius:999px;background:rgba(0,0,0,.5);color:rgba(255,255,255,.9);font-size:12px;font-weight:700;font-variant-numeric:tabular-nums;pointer-events:none}
.viewer-bar,.viewer-index{transition:opacity .2s ease}
.viewer.chrome-hidden :is(.viewer-bar,.viewer-nav,.viewer-index,.viewer-live-badge){opacity:0;pointer-events:none}
.viewer.chrome-hidden .viewer-bar > *{pointer-events:none}
/* Fingers page by swiping; the arrow buttons are for a mouse. */
@media(hover:none) and (pointer:coarse){.viewer-nav{display:none}}
@media(prefers-reduced-motion:reduce){
  .album-item,.album-empty.loading .album-empty-icon,.viewer-spinner{animation:none}
  .album-selectbar{animation:none}
  .album-item img,.album-check{transition:none}
}

/* ---------- Immersive (video fullscreen) ---------- */
body.immersive{overflow:hidden;padding-top:0}
body.immersive #navbar{display:none}
body.immersive main,body.immersive.operating main{max-width:none;padding:0}
body.immersive #stage{height:var(--app-height,100dvh);max-height:none;min-height:0;border-radius:0}
body.immersive .fullscreen-btn{top:calc(10px + env(safe-area-inset-top));right:calc(10px + env(safe-area-inset-right))}
body.immersive #video-status{top:calc(14px + env(safe-area-inset-top))}

/* ---------- Responsive ---------- */
@media(max-width:700px){
  main{padding:12px 14px 24px}
  /* A phone held upright is too narrow for the toolbar and the connection
     status on one line: the toolbar may use the whole width (wrapping if a
     transient button such as 取消追焦 appears), and the status drops below. */
  #stage-toolbar{left:10px;gap:6px;justify-content:flex-end;flex-wrap:wrap;pointer-events:none}
  #stage-toolbar>*{pointer-events:auto}
  #viewer-count{pointer-events:none}
  /* 取消追焦 is transient; while it shows, the viewer count gives way so
     the toolbar stays on one line on most phones. */
  #stage-toolbar:has(#focus-auto:not([hidden])) #viewer-count{display:none}
  #video-status{top:54px}
  body.immersive #stage-toolbar{left:calc(10px + env(safe-area-inset-left))}
  body.immersive #video-status{top:calc(54px + env(safe-area-inset-top))}
  :root{--nav-height:52px}
  body.operating main{padding:8px 14px 12px}
  .actions{gap:14px}
  .round{width:58px;height:58px;padding:16px}
  .album-grid{grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:4px}
  .album-actions .btn{padding:.78571429em 1em}
  .album-selectbar .btn{padding:.78571429em 1.1em}
  .viewer-btn{padding:5px 11px}
}
/* A phone on its side has height to spare for nothing: the bar shrinks to fit
   its tabs, and bar and page both keep clear of a notch at either side. */
@media(orientation:landscape) and (max-height:550px){
  :root{--nav-height:44px}
  .nav-inner{padding:0 max(14px,env(safe-area-inset-right)) 0 max(14px,env(safe-area-inset-left))}
  .tab{padding:5px 12px}
  body.operating main{padding:4px max(14px,env(safe-area-inset-right)) 8px max(14px,env(safe-area-inset-left))}
  .round{width:54px;height:54px;padding:14px}
}
@media(prefers-reduced-motion:reduce){.round.running:before,#snapshot.capturing:before{animation-duration:4s}}

/* ---------- Console login ---------- */
/* Semantic "raised segment" on a plain grey page, with Semantic's own form
   field, fluid primary button and negative message. */
.login-page{min-height:100dvh;padding:0;background:var(--page-bg,#f7f7f7)}
.login-shell{min-height:100dvh;display:grid;place-items:center;padding:24px}
.login-card{width:min(100%,420px);padding:2em;background:#fff;border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-md)}
/* Mark and wordmark sit on one line; the mark carries its own transparent
   padding, so it is pulled left a touch to line its body up with the fields. */
.login-brand{display:flex;align-items:center;gap:6px;margin:0 0 25px}
.login-logo{display:block;width:48px;height:48px;margin-left:-4px}
.login-card h1{margin:0;color:var(--text);font-size:28px;font-weight:700}
#login-form{display:grid;gap:0}
#login-form label{margin-top:1em;margin-bottom:.28571429rem;color:var(--text);font-size:13px;font-weight:700}
#login-form input{width:100%;height:38px;padding:.67857143em 1em;border:1px solid var(--border);border-radius:var(--radius);background:#fff;color:var(--text);font:inherit;outline:none;transition:border-color .1s ease}
#login-form input:focus{border-color:var(--focus-border);box-shadow:none}
.login-submit{width:100%;height:38px;margin-top:1.5em}
.login-submit:disabled{opacity:.45;cursor:wait}
.login-error{margin:1em 0 0;padding:1em 1.5em;border-radius:var(--radius);background:var(--negative-tint);box-shadow:0 0 0 1px var(--negative-border) inset;color:var(--negative-text);font-size:13px;font-weight:700}

@media(max-width:500px){.login-shell{padding:16px}.login-card{padding:28px 22px}}

/* ---------- Confirmation dialog ---------- */
/* The page's own confirm, over everything -- the photo viewer included. */
body.dialog-open{overflow:hidden}
.dialog{position:fixed;inset:0;z-index:300;display:flex;align-items:center;justify-content:center;padding:calc(16px + env(safe-area-inset-top)) calc(16px + env(safe-area-inset-right)) calc(16px + env(safe-area-inset-bottom)) calc(16px + env(safe-area-inset-left));background:rgba(0,0,0,.45);animation:dialog-fade .15s ease-out}
.dialog[hidden]{display:none}
.dialog-card{width:min(100%,400px);padding:22px 22px 18px;background:#fff;color:var(--text);border-radius:calc(var(--radius) * 2);box-shadow:0 12px 40px rgba(0,0,0,.3);animation:dialog-in .18s cubic-bezier(.2,.8,.2,1)}
.dialog-title{font-size:17px;line-height:1.4}
.dialog-text{margin-top:8px;color:var(--text-muted);font-size:14px;line-height:1.6}
.dialog-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}
.dialog-actions .btn{min-width:84px}
@media(max-width:480px){.dialog-actions .btn{flex:1}}
@keyframes dialog-fade{from{opacity:0}}
@keyframes dialog-in{from{opacity:0;transform:scale(.96)}}
@media(prefers-reduced-motion:reduce){.dialog,.dialog-card{animation:none}}
