:root {
  --bg: #0b0d12;
  --panel: #161a22;
  --panel2: #1e2430;
  --text: #eef1f6;
  --muted: #93a0b4;
  --accent: #5b8cff;
  --good: #35d07f;
  --warn: #f5a623;
  --bad: #ef5350;
  --radius: 14px;
  font-size: 16px;
}
* { box-sizing: border-box; }
html, body {
  margin: 0; padding: 0; height: 100%;
  background: var(--bg); color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  -webkit-tap-highlight-color: transparent;
}
#app { min-height: 100%; display: flex; flex-direction: column; }
button {
  font: inherit; color: inherit; cursor: pointer;
  border: none; border-radius: 10px; padding: .8em 1.2em;
  background: var(--panel2);
}
button:disabled { opacity: .4; cursor: not-allowed; }
button.primary { background: var(--accent); color: #08101f; font-weight: 700; }
button.good { background: var(--good); color: #06301a; font-weight: 700; }
/* textarea belongs here too. Left out, it keeps the browser default white box,
   which on a dark panel reads as a rendering fault rather than a text field. */
input, select, textarea {
  font: inherit; color: var(--text); background: var(--panel2);
  border: 1px solid #2c3444; border-radius: 10px; padding: .7em .9em; width: 100%;
}
input::placeholder, textarea::placeholder { color: var(--muted); }
input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: none; border-color: var(--accent);
}
a { color: var(--accent); }

.screen { flex: 1; display: flex; flex-direction: column; }
.container { max-width: 720px; margin: 0 auto; width: 100%; padding: 20px; }
.topbar {
  display: flex; align-items: center; gap: 12px; padding: 14px 20px;
  border-bottom: 1px solid #1f2530; position: sticky; top: 0; background: var(--bg); z-index: 5;
}
.topbar h1 { font-size: 1.1rem; margin: 0; flex: 1; }
.topbar .back {
  background: none; padding: 0;
  /* A 30px-tall text button is an awkward tap target on a phone; 44px is the
     usual minimum, and flex keeps the arrow centred inside it. */
  width: 44px; height: 44px; margin-left: -10px;
  display: flex; align-items: center; justify-content: center;
  font-size: 20px; line-height: 1;
}
.muted { color: var(--muted); }
.card {
  background: var(--panel); border-radius: var(--radius); padding: 16px; margin-bottom: 12px;
}
.field { margin-bottom: 14px; }
.field label { display: block; margin-bottom: 6px; color: var(--muted); font-size: .9rem; }
.mode-row { display: flex; gap: 10px; }
.mode-btn {
  flex: 1; text-align: left; background: var(--panel2); border: 2px solid transparent;
  border-radius: 12px; padding: 14px;
}
.mode-btn.selected { border-color: var(--accent); background: #1a2440; }
.mode-btn .title { font-weight: 700; }
.mode-btn .desc { color: var(--muted); font-size: .85rem; margin-top: 4px; }

.capture-list { display: flex; flex-direction: column; gap: 8px; }
.capture-row {
  display: flex; align-items: center; gap: 12px; background: var(--panel); border-radius: 12px; padding: 12px;
}
.capture-row .thumb {
  width: 56px; height: 56px; border-radius: 8px; background: var(--panel2);
  background-size: cover; background-position: center; flex-shrink: 0;
}
.capture-row .meta { flex: 1; min-width: 0; }
.capture-row .meta .t { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.capture-row .meta .s { color: var(--muted); font-size: .82rem; }
.badge {
  font-size: .72rem; padding: .2em .6em; border-radius: 999px; background: var(--panel2); color: var(--muted);
}
.badge.ready { background: #103a25; color: var(--good); }
.badge.processing { background: #3a3010; color: var(--warn); }
.badge.failed { background: #3a1414; color: var(--bad); }

/* ---------- Capture screen ---------- */
.capture-screen { position: relative; flex: 1; background: #000; overflow: hidden; }
#camVideo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.cam-overlay { position: absolute; inset: 0; display: flex; flex-direction: column; pointer-events: none; }
.cam-overlay > * { pointer-events: auto; }

.cam-top {
  display: flex; align-items: center; gap: 10px; padding: 14px 16px;
  background: linear-gradient(to bottom, rgba(0,0,0,.65), transparent);
}
.cam-top .back { background: rgba(0,0,0,.4); }
.counter { margin-left: auto; background: rgba(0,0,0,.5); padding: .3em .8em; border-radius: 999px; font-variant-numeric: tabular-nums; }

.finish-row { display: flex; justify-content: center; }
.finish-row button { width: 100%; max-width: 420px; }

.center-msg {
  margin: auto; text-align: center; padding: 24px; max-width: 480px; color: var(--text);
}
.center-msg h2 { margin-top: 0; }
.center-msg code { background: var(--panel2); padding: .1em .4em; border-radius: 6px; }

/* ---------- Processing screen ---------- */
.progress-wrap { background: var(--panel2); border-radius: 999px; height: 14px; overflow: hidden; margin: 16px 0; }
.progress-bar { height: 100%; background: linear-gradient(90deg, var(--accent), var(--good)); width: 0%; transition: width .3s; }
.frame-ticks { display: flex; flex-wrap: wrap; gap: 6px; margin: 14px 0; }
.tick { width: 14px; height: 14px; border-radius: 4px; background: var(--panel2); }
.tick.done { background: var(--good); }
.tick.failed { background: var(--bad); }
.tick.pending { background: var(--panel2); }
.log { background: #000; border-radius: 10px; padding: 10px 12px; font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: .8rem; color: #9fd; height: 160px; overflow-y: auto; }
.log div { white-space: pre-wrap; opacity: .9; }
.banner { border-radius: 10px; padding: 12px 14px; margin-bottom: 14px; }
.banner.warn { background: #3a3010; color: var(--warn); }
.banner.bad { background: #3a1414; color: var(--bad); }
.banner.good { background: #103a25; color: var(--good); }

/* ---------- Viewer screen ---------- */
.viewer-screen { position: relative; flex: 1; background: #000; overflow: hidden; touch-action: none; }
#viewerCanvasHost { position: absolute; inset: 0; }
.viewer-hud {
  position: absolute; top: 0; left: 0; right: 0; display: flex; align-items: center; gap: 10px; padding: 14px 16px;
  background: linear-gradient(to bottom, rgba(0,0,0,.6), transparent); z-index: 3;
}
.viewer-hud .title { font-weight: 700; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.viewer-loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 12px; color: #fff; z-index: 4; }
.spinner { width: 40px; height: 40px; border-radius: 50%; border: 4px solid rgba(255,255,255,.2); border-top-color: var(--accent); animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.share-box { position: absolute; bottom: 16px; left: 16px; right: 16px; background: rgba(10,12,18,.85);
  border-radius: 12px; padding: 10px 12px; display: flex; gap: 8px; align-items: center; z-index: 3; }
.share-box input { flex: 1; }

footer.hint { text-align: center; color: var(--muted); font-size: .8rem; padding: 10px; }

/* --- delete control on a capture row --- */
.capture-row { position: relative; }

.row-delete,
.row-rebuild,
.row-download {
  flex: 0 0 auto;
  width: 30px; height: 30px;
  margin-left: 6px;
  border: 0; border-radius: 50%;
  background: rgba(255,255,255,.07);
  color: #9aa4b2;
  font-size: 19px; line-height: 1;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background .15s, color .15s, transform .15s;
}
.row-delete:hover,
.row-delete:focus-visible {
  background: rgba(229,72,77,.18);
  color: #ff6b6b;
  transform: scale(1.08);
  outline: none;
}
/* Rebuilding is safe and repeatable, so it reads as an ordinary action rather
   than borrowing the delete button's warning colour. */
.row-rebuild:hover,
.row-rebuild:focus-visible,
.row-download:hover,
.row-download:focus-visible {
  background: rgba(96,141,255,.20);
  color: #9ab6ff;
  transform: scale(1.08);
  outline: none;
}
.row-rebuild:disabled,
.row-download:disabled { opacity: .45; cursor: default; transform: none; }

/* Touch targets need to be bigger than the visual circle on a phone. */
@media (pointer: coarse) {
  .row-delete,
  .row-rebuild,
  .row-download { width: 38px; height: 38px; font-size: 22px; }
}

.capture-row.confirming {
  background: rgba(229,72,77,.10);
  border-color: rgba(229,72,77,.35);
  cursor: default;
}
.capture-row.confirming .t { color: #ffb4b4; }

.row-cancel, .row-confirm {
  flex: 0 0 auto;
  margin-left: 8px; padding: 8px 14px;
  border-radius: 8px; border: 1px solid transparent;
  font-size: 13px; font-weight: 600; cursor: pointer;
}
.row-cancel  { background: rgba(255,255,255,.08); color: #d7dce3; }
.row-confirm { background: #e5484d; color: #fff; }
.row-confirm:disabled { opacity: .6; cursor: default; }

.capture-row.removing {
  opacity: 0; height: 0 !important;
  padding-top: 0; padding-bottom: 0; margin-bottom: 0;
  overflow: hidden;
  transition: opacity .18s ease, height .2s ease, padding .2s ease, margin .2s ease;
}

/* Direction warnings are advice the user can act on, not hard failures. */
.err.warn, #errBox.warn {
  color: #ffd27a;
  background: rgba(255, 176, 32, .10);
  border: 1px solid rgba(255, 176, 32, .30);
  border-radius: 8px;
  padding: 10px 12px;
  line-height: 1.45;
}

/* --- framing grid over the viewfinder --- */
.cam-grid {
  position: absolute; inset: 0;
  pointer-events: none;          /* never steals a tap from the shutter */
  z-index: 2;
}
.cam-grid .v, .cam-grid .h {
  position: absolute;
  background: rgba(255,255,255,.28);
  box-shadow: 0 0 2px rgba(0,0,0,.5);   /* stays visible over a bright scene */
}
.cam-grid .v { top: 0; bottom: 0; width: 1px; }
.cam-grid .h { left: 0; right: 0; height: 1px; }
.cam-grid .v1 { left: 33.333%; }
.cam-grid .v2 { left: 66.666%; }
.cam-grid .h1 { top: 33.333%; }
.cam-grid .h2 { top: 66.666%; }

/* The horizon is the important line, so it is brighter than the thirds. */
.cam-grid .horizon {
  position: absolute; left: 0; right: 0; top: 50%;
  height: 2px; margin-top: -1px;
  background: rgba(255,255,255,.5);
  box-shadow: 0 0 4px rgba(0,0,0,.6);
}
.cam-grid.tilted .horizon { background: rgba(255,176,32,.55); }

/* Level indicator: rides above/below the horizon as the phone tips. */
.level-bubble {
  position: absolute; left: 50%; top: 50%;
  width: 82px; height: 3px; margin-top: -1.5px;
  border-radius: 2px;
  background: #ffb020;
  box-shadow: 0 0 8px rgba(0,0,0,.5);
  transform: translate(-50%, 0);
  transition: transform .08s linear, background .15s, box-shadow .15s;
}
.level-bubble.level {
  background: #3ddc84;
  box-shadow: 0 0 10px rgba(61,220,132,.6);
}

.grid-btn {
  width: auto; border-radius: 10px; padding: .35em .7em;
  font-size: 17px; line-height: 1;
}
.grid-btn.active { background: rgba(255,255,255,.22); color: #fff; }

/* --- free-upload progress --- */
.upload-bar {
  margin-top: 8px; height: 6px; border-radius: 3px;
  background: rgba(255,255,255,.10); overflow: hidden;
}
.upload-bar > span {
  display: block; height: 100%; width: 0;
  background: linear-gradient(90deg, #4c8dff, #3ddc84);
  transition: width .2s ease;
}

/* --- compass status readout --- */
.compass-status {
  margin: 6px auto 0;
  padding: 4px 12px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  width: fit-content;
  max-width: 90%;
  text-align: center;
  background: rgba(0,0,0,.45);
  backdrop-filter: blur(4px);
}
.compass-status.on       { color: #3ddc84; }
.compass-status.relative { color: #ffd27a; }   /* working, but not true north */
.compass-status.off      { color: #9aa4b2; }

/* --- blue-dot capture overlay --- */
.dot-canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;      /* dots are guidance, never tap targets */
  z-index: 3;
}

/* Shutter flash, so an auto-fired shot is obviously a shot. */
.shot-flash {
  position: absolute; inset: 0;
  background: #fff; opacity: 0;
  pointer-events: none; z-index: 6;
}
.shot-flash.go { animation: orbit-flash .28s ease-out; }
@keyframes orbit-flash {
  0%   { opacity: .85; }
  100% { opacity: 0; }
}

/* Sensor status line. Colour says at a glance whether tracking is real. */
.status-pill {
  margin: 8px auto 0;
  padding: 5px 14px;
  border-radius: 999px;
  font-size: 12px; font-weight: 600;
  width: fit-content; max-width: 92%;
  text-align: center;
  background: rgba(0,0,0,.5);
  backdrop-filter: blur(4px);
  color: #d7dce3;
}
.status-pill.on   { color: #3ddc84; }
.status-pill.warn { color: #ffd27a; }
.status-pill.off  { color: #9aa4b2; }

.cam-title {
  flex: 1; text-align: center;
  font-size: 14px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.cam-err:empty { display: none; }

/* The shutter glows green while a dot is held inside the reticle. */
.shutter.aligned {
  box-shadow: 0 0 0 5px rgba(61,220,132,.55), 0 0 22px rgba(61,220,132,.5);
  border-color: #3ddc84;
}

@media (pointer: coarse) {
  .side-btn { min-width: 44px; min-height: 44px; }
}

/* ============================================================
   Responsive layout
   The mode picker was a fixed flex row: three cards side by side
   squeezed into ~110px each on a phone, with the text unreadable.
   It now stacks, and everything else scales with the viewport.
   ============================================================ */

.mode-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 10px;
}
.mode-btn {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 76px;
  text-align: left;
}

@media (max-width: 640px) {
  .container { padding: 14px; }
  .card { padding: 14px; border-radius: 12px; }
  .topbar h1 { font-size: 1.15rem; }

  /* One card per row, laid out horizontally so each stays short. */
  .mode-row { grid-template-columns: 1fr; }
  .mode-btn { min-height: 0; padding: 12px 14px; }
  .mode-btn .desc { font-size: .8rem; }

  .capture-row { gap: 8px; }
  .capture-row .thumb { width: 52px; height: 52px; }
  .capture-row .t { font-size: .95rem; }
  .capture-row .s { font-size: .75rem; }

  /* Stack the inline delete confirmation instead of letting the buttons
     shove the title off the row. */
  .capture-row.confirming { flex-wrap: wrap; }
  .capture-row.confirming .meta { flex: 1 1 100%; margin-bottom: 8px; }
  .row-cancel, .row-confirm { flex: 1 1 auto; margin-left: 0; }
  .row-confirm { margin-left: 8px; }
}

@media (max-width: 380px) {
  .container { padding: 10px; }
  .mode-btn .desc { display: none; }   /* titles alone are clear enough here */
}

/* Never let a wide element force the whole page to scroll sideways. */
html, body { max-width: 100%; overflow-x: hidden; }
img, canvas, video { max-width: 100%; }

/* ============================================================
   Capture screen
   ============================================================ */

.capture-screen {
  position: fixed; inset: 0;
  background: #000;
  overflow: hidden;
}
#camVideo {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;            /* fill the phone screen, never letterbox */
}
/* The overlap strip from the previous photo. Only the edge the next shot will
   share is shown, so it can be judged against rather than seen through -
   which is why this is far less transparent than a full-frame ghost could be.
   The dashed rule marks where the strip ends and the live camera begins. */
#ghostImg {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: .55;
  pointer-events: none;
  z-index: 1;
}
#ghostImg[data-side="left"]  { border-right: 2px dashed rgba(255,255,255,.65); }
#ghostImg[data-side="right"] { border-left:  2px dashed rgba(255,255,255,.65); }

.cam-overlay {
  position: absolute; inset: 0;
  z-index: 4;
  display: flex;
  flex-direction: column;
  /* Keep controls clear of the notch and the home indicator. */
  padding: max(10px, env(safe-area-inset-top)) 12px
           max(14px, env(safe-area-inset-bottom));
  pointer-events: none;
}
.cam-overlay > * { pointer-events: auto; }

.cam-top {
  display: flex; align-items: center; gap: 8px;
  background: linear-gradient(rgba(0,0,0,.55), transparent);
  margin: calc(-1 * max(10px, env(safe-area-inset-top))) -12px 0;
  padding: max(10px, env(safe-area-inset-top)) 12px 22px;
}
.cam-top .back {
  background: rgba(0,0,0,.45); color: #fff; border: 0;
  width: 44px; height: 44px; border-radius: 50%;
  font-size: 20px; line-height: 1; cursor: pointer;
  /* Without this it inherits the global button padding, which inflates the
     circle and leaves the arrow sitting low and right of centre. */
  display: flex; align-items: center; justify-content: center; padding: 0;
}
.counter {
  background: rgba(0,0,0,.5); border-radius: 999px;
  padding: 5px 12px; font-size: 12px; font-weight: 700;
  white-space: nowrap;
}

/* The instruction panel sits low so it never covers the reticle. */
.target-panel {
  margin-top: auto;
  text-align: center;
  padding: 10px 14px;
  background: rgba(0,0,0,.5);
  backdrop-filter: blur(6px);
  border-radius: 14px;
  align-self: center;
  max-width: 92%;
}
.target-label { font-size: 1.15rem; font-weight: 700; }
.target-hint  { font-size: .85rem; color: #c8cfd8; margin-top: 3px; line-height: 1.35; }

.thumb-strip {
  display: flex; gap: 6px; overflow-x: auto; overflow-y: hidden;
  padding: 8px 2px; margin: 6px 0;
  scrollbar-width: none;
}
.thumb-strip::-webkit-scrollbar { display: none; }
.thumb-strip img {
  width: 46px; height: 46px; object-fit: cover;
  border-radius: 7px; border: 2px solid rgba(255,255,255,.55);
  flex: 0 0 auto;
}

/* Three fixed columns so the shutter stays dead centre whether or not the
   side buttons are showing. With plain flex centring, hiding one shifted the
   shutter across the screen. */
.cam-bottom {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  padding: 6px 0 2px;
}
.cam-bottom > :first-child { justify-self: end;   margin-right: 26px; }
.cam-bottom > :last-child  { justify-self: start; margin-left: 26px; }
.cam-bottom > .shutter     { justify-self: center; }
.shutter {
  width: 74px; height: 74px; border-radius: 50%;
  background: rgba(255,255,255,.95);
  border: 5px solid rgba(255,255,255,.55);
  cursor: pointer;
  transition: transform .1s, box-shadow .18s, border-color .18s;
}
.shutter:active { transform: scale(.93); }
.shutter:disabled { opacity: .5; }
.side-btn {
  width: 46px; height: 46px; border-radius: 50%;
  background: rgba(0,0,0,.45); color: #fff; border: 0;
  font-size: 19px; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.side-btn:disabled { opacity: .35; }

.finish-btn {
  width: 100%; margin-top: 8px;
  padding: 14px; font-size: 1rem; font-weight: 700;
  border-radius: 12px;
}
.finish-btn:disabled { opacity: .45; }

.cam-err {
  background: rgba(0,0,0,.6); border-radius: 10px;
  padding: 9px 12px; font-size: .85rem; line-height: 1.4;
  margin: 0 auto; max-width: 92%; text-align: center;
}

/* Landscape: the panel and strip would eat the whole screen. */
@media (orientation: landscape) and (max-height: 500px) {
  .target-hint { display: none; }
  .target-panel { padding: 6px 12px; }
  .thumb-strip img { width: 34px; height: 34px; }
  .shutter { width: 58px; height: 58px; border-width: 4px; }
  .cam-bottom { gap: 18px; }
  .finish-btn { padding: 10px; }
}

/* ============================================================
   Camera: say one thing at a time
   The old overlay showed a sensor readout, an icon, a label, a hint,
   a counter and a compass strip at once. Almost none of it was
   something the user could act on.
   ============================================================ */

/* Everything below the viewfinder sits together at the bottom. */
.cam-foot { margin-top: auto; }

/* Quiet when things work; visible only when the user must do something. */
.status-pill.hidden { opacity: 0; pointer-events: none; }
.status-pill { transition: opacity .2s; }

@media (max-width: 380px) {    }
@media (orientation: landscape) and (max-height: 500px) {  }

/* Author styles that set `display` beat the browser's own rule for the hidden
   attribute, so .side-btn{display:flex} was keeping buttons on screen after
   JavaScript had hidden them - the "set front" control showed even on a phone
   with no motion sensor. */
[hidden] { display: none !important; }

/* The finish button was dark text on a muted fill while disabled. */
.finish-btn:disabled {
  background: rgba(255, 255, 255, .14);
  color: #cfd6df;
  opacity: 1;
}

/* Coverage warning above the finish button. Anything the camera never pointed
   at is filled with a soft blur in the result, so the warning has to land
   while the user can still go and shoot it. */
.cover-warn {
  margin: 0 auto 8px;
  max-width: 420px;
  padding: 7px 12px;
  border-radius: 10px;
  font-size: 12px; font-weight: 600; line-height: 1.35;
  text-align: center;
  color: #ffd27a;
  background: rgba(0, 0, 0, .55);
  backdrop-filter: blur(4px);
}
.cover-warn[hidden] { display: none; }

/* --- panorama viewer controls ---
   A single bar rather than buttons scattered over the image: everything the
   viewer can do is in one place, and it stays clear of the share box below and
   the title above. */
.viewer-tools {
  position: absolute;
  right: 14px; top: 50%;
  transform: translateY(-50%);
  z-index: 3;
  display: flex; flex-direction: column; gap: 8px;
}
.viewer-tool {
  width: 42px; height: 42px;
  padding: 0; border: 0; border-radius: 50%;
  background: rgba(10, 12, 18, .55);
  backdrop-filter: blur(6px);
  color: #fff; font-size: 17px; line-height: 1;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  transition: background .15s, transform .15s, opacity .15s;
}
.viewer-tool:hover,
.viewer-tool:focus-visible { background: rgba(96,141,255,.45); transform: scale(1.07); outline: none; }
/* A toggle that is off should look off, not merely unpressed. */
.viewer-tool.off { opacity: .45; }

/* Landscape on a phone is short: a vertical bar would run off the screen. */
@media (orientation: landscape) and (max-height: 480px) {
  .viewer-tools {
    top: auto; bottom: 10px; right: 50%;
    transform: translateX(50%);
    flex-direction: row;
  }
  .viewer-tool { width: 38px; height: 38px; font-size: 15px; }
}

/* ------------------------------------------------------------------ *
   Hotspots

   The panorama engine positions these absolutely and hides them when they
   swing behind the camera, so nothing here concerns itself with placement -
   only with what a marker looks like once it is put somewhere.

   Everything is sized in rem and anchored with translate(-50%, -50%),
   because the coordinate the engine sets is the point in the WORLD the
   hotspot marks, and that should sit at the middle of the marker rather
   than its top-left corner.
 * ------------------------------------------------------------------ */
.hotspot {
  position: absolute;
  transform: translate(-50%, -50%);
  z-index: 5;
}

.hotspot-tooltip {
  position: absolute;
  left: 50%; bottom: calc(100% + 10px);
  transform: translateX(-50%);
  background: rgba(10, 12, 18, .92);
  backdrop-filter: blur(6px);
  color: #fff;
  padding: 6px 11px;
  border-radius: 8px;
  font-size: .82rem; font-weight: 600;
  white-space: nowrap;
  opacity: 0; pointer-events: none;
  transition: opacity .15s;
}
.hotspot:hover .hotspot-tooltip { opacity: 1; }

/* --- link: an arrow you walk through --- */
.link-hotspot { cursor: pointer; }
.link-hotspot-icon {
  width: 52px; height: 52px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  color: #fff;
  background: rgba(91, 140, 255, .30);
  border: 2px solid rgba(255, 255, 255, .85);
  backdrop-filter: blur(3px);
  transition: transform .18s, background .18s;
  /* The arrow is rotated inline to point along the floor, so this element
     must not also be the one that scales - the two transforms would fight.
     Hence the scale living on :hover of the PARENT, applied to a child. */
}
.link-hotspot:hover .link-hotspot-icon { background: rgba(91, 140, 255, .55); }
.link-hotspot::after {
  /* A soft pulse, so an exit is noticeable without being animated clutter. */
  content: "";
  position: absolute; inset: -6px;
  border-radius: 50%;
  border: 2px solid rgba(91, 140, 255, .55);
  animation: hotspot-pulse 2.4s ease-out infinite;
  pointer-events: none;
}
@keyframes hotspot-pulse {
  0%   { transform: scale(.85); opacity: .8; }
  70%  { transform: scale(1.25); opacity: 0; }
  100% { transform: scale(1.25); opacity: 0; }
}

/* --- info: a dot that opens a panel --- */
.info-hotspot-header {
  display: flex; align-items: center; gap: 8px;
  padding: 0; border: 0; border-radius: 999px;
  background: rgba(10, 12, 18, .70);
  backdrop-filter: blur(6px);
  border: 2px solid rgba(255, 255, 255, .8);
  color: #fff;
  cursor: pointer;
  overflow: hidden;
  max-width: 44px;
  transition: max-width .25s ease, background .18s;
}
.info-hotspot-icon-wrapper {
  width: 40px; height: 40px;
  flex: 0 0 40px;
  display: flex; align-items: center; justify-content: center;
}
.info-hotspot-title-wrapper {
  padding-right: 14px;
  font-size: .85rem; font-weight: 700;
  white-space: nowrap;
}
/* The label unrolls out of the dot on hover. max-width rather than width so
   the transition has something to animate between. */
.info-hotspot-header:hover,
.info-hotspot-header:focus-visible {
  max-width: 320px;
  background: rgba(91, 140, 255, .6);
  outline: none;
}
.info-hotspot.open .info-hotspot-header { max-width: 320px; }

.info-hotspot-panel {
  display: none;
  position: absolute;
  left: 50%; top: calc(100% + 12px);
  transform: translateX(-50%);
  width: min(320px, 78vw);
  background: rgba(16, 20, 28, .96);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, .12);
  border-radius: var(--radius);
  box-shadow: 0 18px 50px rgba(0, 0, 0, .55);
  color: var(--text);
  text-align: left;
}
.info-hotspot.open .info-hotspot-panel { display: block; }
.info-hotspot-panel-head {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 12px 6px 14px;
}
.info-hotspot-panel-head h3 {
  margin: 0; flex: 1;
  font-size: .95rem; font-weight: 700;
}
.info-hotspot-close {
  padding: 0; width: 28px; height: 28px;
  border-radius: 50%;
  background: transparent; color: var(--muted);
  display: flex; align-items: center; justify-content: center;
}
.info-hotspot-close:hover { background: var(--panel2); color: var(--text); }
.info-hotspot-text {
  padding: 0 14px 14px;
  font-size: .86rem; line-height: 1.5;
  color: var(--muted);
  max-height: 40vh; overflow-y: auto;
  white-space: pre-wrap;
}

/* On a phone the panel cannot hang off a marker near the edge of the screen,
   so it becomes a sheet at the bottom instead. Same element, same handlers -
   only the styling differs. */
@media (max-width: 600px) {
  .info-hotspot-panel {
    position: fixed;
    left: 0; right: 0; bottom: 0; top: auto;
    transform: none;
    width: auto;
    border-radius: var(--radius) var(--radius) 0 0;
    z-index: 40;
  }
  .info-hotspot-text { max-height: 45vh; }
}

/* --- editing --- */
.hotspot-edit {
  position: absolute;
  left: 50%; top: calc(100% + 8px);
  transform: translateX(-50%);
  display: flex; gap: 6px;
  opacity: 0; pointer-events: none;
  transition: opacity .15s;
}
.hotspot:hover .hotspot-edit { opacity: 1; pointer-events: auto; }
.hotspot-edit-btn {
  width: 30px; height: 30px; padding: 0;
  border-radius: 50%;
  background: rgba(10, 12, 18, .85);
  color: #fff;
  display: flex; align-items: center; justify-content: center;
}
.hotspot-edit-btn:hover { background: var(--accent); color: #08101f; }
.hotspot-edit-btn.danger:hover { background: var(--bad); color: #fff; }

/* While placing, the panorama takes a crosshair so it is obvious that a click
   does something other than look around. */
.viewer-screen.placing { cursor: crosshair; }

/* ------------------------------------------------------------------ *
   Scene list — the places in a tour
 * ------------------------------------------------------------------ */
.scene-list {
  position: absolute;
  left: 14px; top: 64px;
  z-index: 6;
  max-height: calc(100% - 150px);
  overflow-y: auto;
  display: flex; flex-direction: column; gap: 4px;
  padding: 10px;
  border-radius: var(--radius);
  background: rgba(10, 12, 18, .72);
  backdrop-filter: blur(8px);
  max-width: 220px;
}
.scene-list-title {
  font-size: .7rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted);
  padding: 2px 8px 6px;
}
.scene-item {
  text-align: left;
  padding: 8px 10px;
  border-radius: 9px;
  background: transparent;
  font-size: .85rem;
  color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.scene-item:hover { background: rgba(255, 255, 255, .08); }
.scene-item.active { background: var(--accent); color: #08101f; font-weight: 700; }

/* A phone in portrait has no room for a sidebar next to a 360, so the list
   becomes a strip that scrolls sideways along the top. */
@media (max-width: 600px) {
  .scene-list {
    left: 10px; right: 10px; top: 60px;
    max-width: none;
    flex-direction: row;
    overflow-x: auto; overflow-y: hidden;
    padding: 8px;
  }
  .scene-list-title { display: none; }
  .scene-item { flex: 0 0 auto; }
}

/* ------------------------------------------------------------------ *
   Edit bar
 * ------------------------------------------------------------------ */
.edit-bar {
  position: absolute;
  left: 50%; top: 64px;
  transform: translateX(-50%);
  z-index: 6;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
}
.edit-toggle {
  padding: .55em 1.1em;
  border-radius: 999px;
  background: rgba(10, 12, 18, .75);
  backdrop-filter: blur(8px);
  font-size: .85rem; font-weight: 600;
}
.edit-toggle.active { background: var(--accent); color: #08101f; }
.edit-hint {
  padding: 6px 12px;
  border-radius: 999px;
  background: rgba(10, 12, 18, .75);
  backdrop-filter: blur(8px);
  font-size: .78rem;
  color: var(--muted);
}

/* ------------------------------------------------------------------ *
   Hotspot editor dialog
 * ------------------------------------------------------------------ */
.he-backdrop {
  position: fixed; inset: 0;
  z-index: 60;
  background: rgba(4, 6, 10, .6);
  backdrop-filter: blur(3px);
  display: flex; align-items: center; justify-content: center;
  padding: 16px;
}
.he-card {
  width: min(440px, 100%);
  max-height: 90vh; overflow-y: auto;
  background: var(--panel);
  border: 1px solid rgba(255, 255, 255, .1);
  border-radius: var(--radius);
  box-shadow: 0 24px 70px rgba(0, 0, 0, .6);
  padding: 20px;
}
.he-card h2 { margin: 0 0 16px; font-size: 1.1rem; }

.he-kinds { display: flex; gap: 10px; margin-bottom: 18px; }
.he-kind {
  flex: 1;
  display: flex; flex-direction: column; gap: 3px;
  align-items: flex-start; text-align: left;
  padding: 11px 13px;
  border-radius: 11px;
  background: var(--panel2);
  border: 2px solid transparent;
}
.he-kind strong { font-size: .88rem; }
.he-kind small { color: var(--muted); font-size: .74rem; line-height: 1.3; }
.he-kind.active { border-color: var(--accent); background: rgba(91, 140, 255, .14); }

.he-field { display: block; margin-bottom: 14px; }
.he-field > span {
  display: block;
  font-size: .8rem; font-weight: 600;
  margin-bottom: 5px;
}
.he-field > small {
  display: block;
  color: var(--muted); font-size: .74rem;
  margin: -3px 0 6px;
}
.he-field input[type="text"],
.he-field textarea,
.he-field select { width: 100%; }
.he-field textarea { resize: vertical; min-height: 76px; font: inherit; }

.he-rot { display: flex; align-items: center; gap: 14px; }
.he-rot input[type="range"] { flex: 1; }
.he-rot-preview {
  width: 44px; height: 44px; flex: 0 0 44px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: rgba(91, 140, 255, .2);
  border: 2px solid rgba(255, 255, 255, .5);
  color: #fff;
}

.he-error {
  background: #3a1414; color: var(--bad);
  padding: 9px 12px; border-radius: 9px;
  font-size: .82rem;
  margin-bottom: 12px;
}
.he-actions { display: flex; align-items: center; gap: 10px; }
.he-delete { background: transparent; color: var(--bad); padding-left: 0; }
.he-delete:hover { background: #3a1414; }

/* ------------------------------------------------------------------ *
   "Why does this look like this?"

   The explanation for a degraded 360 used to be a banner across the top
   of the view. It is worth saying - it tells someone their capture came
   up short and how to reshoot it - but it is several lines long, and as
   a banner it covered the picture they came to look at. A dot they can
   tap says the same thing without standing in front of it.
 * ------------------------------------------------------------------ */
#degradedBanner { position: absolute; top: 64px; left: 14px; z-index: 6; }

.degraded-btn {
  width: 30px; height: 30px; padding: 0;
  border-radius: 50%;
  background: rgba(245, 166, 35, .92);
  color: #2a1e05;
  font-weight: 800; font-size: .9rem; font-style: italic;
  font-family: Georgia, "Times New Roman", serif;
  line-height: 1;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 2px 10px rgba(0, 0, 0, .4);
}
.degraded-btn:hover,
.degraded-btn:focus-visible { transform: scale(1.1); outline: none; }

.degraded-note {
  position: absolute;
  top: calc(100% + 8px); left: 0;
  width: min(320px, calc(100vw - 40px));
  background: rgba(16, 20, 28, .97);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(245, 166, 35, .35);
  border-radius: var(--radius);
  box-shadow: 0 18px 50px rgba(0, 0, 0, .55);
  padding: 13px 15px;
}
.degraded-note p {
  margin: 0 0 11px;
  font-size: .84rem; line-height: 1.5;
  color: var(--text);
}
.degraded-close {
  padding: .45em 1em;
  font-size: .8rem; font-weight: 600;
  background: var(--panel2);
}

/* ------------------------------------------------------------------ *
   "Why does this look like this?"

   The explanation for a degraded 360 used to be a banner across the top
   of the view. It is worth saying - it tells someone their capture came
   up short and how to reshoot it - but it is several lines long, and as
   a banner it covered the picture they came to look at. A dot they can
   tap says the same thing without standing in front of it.
 * ------------------------------------------------------------------ */
#degradedBanner { position: absolute; top: 64px; left: 14px; z-index: 6; }

.degraded-btn {
  width: 30px; height: 30px; padding: 0;
  border-radius: 50%;
  background: rgba(245, 166, 35, .92);
  color: #2a1e05;
  font-weight: 800; font-size: .9rem; font-style: italic;
  font-family: Georgia, "Times New Roman", serif;
  line-height: 1;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 2px 10px rgba(0, 0, 0, .4);
}
.degraded-btn:hover,
.degraded-btn:focus-visible { transform: scale(1.1); outline: none; }

.degraded-note {
  position: absolute;
  top: calc(100% + 8px); left: 0;
  width: min(320px, calc(100vw - 40px));
  background: rgba(16, 20, 28, .97);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(245, 166, 35, .35);
  border-radius: var(--radius);
  box-shadow: 0 18px 50px rgba(0, 0, 0, .55);
  padding: 13px 15px;
}
.degraded-note p {
  margin: 0 0 11px;
  font-size: .84rem; line-height: 1.5;
  color: var(--text);
}
.degraded-close {
  padding: .45em 1em;
  font-size: .8rem; font-weight: 600;
  background: var(--panel2);
}


/* Auto-shoot toggle, next to the framing grid. Dimmed when off, so a glance at
   the top bar says whether the shutter will fire by itself. */
.auto-btn {
  width: auto; border-radius: 10px; padding: .35em .7em;
  font-size: 15px; font-weight: 700; line-height: 1; letter-spacing: .04em;
  opacity: .55;
}
/* Green when auto-shoot is armed - the same green the shutter turns when a dot
   is centred, so "green means it is about to fire by itself" is one idea on
   this screen rather than two. */
.auto-btn.active {
  background: #3ddc84; color: #06210f; opacity: 1;
  box-shadow: 0 0 12px rgba(61,220,132,.45);
}
