/* Reset, page frame, and the shared control primitives. */

* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }

body {
  font: 14px/1.45 var(--sans);
  background: var(--bg); color: var(--fg);
  -webkit-font-smoothing: antialiased;
}

/* Vue renders a fragment of siblings into #app, so #app is the flex column
   that used to be <body>. */
#app {
  min-height: 100%;
  display: flex;
  flex-direction: column;
}

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

code { font-family: var(--mono); font-size: 0.92em; }

/* --minhit is the tap-target floor. 36px is comfortable with a mouse; a
   coarse pointer gets 44px, which is the size a thumb actually hits — this
   page is used one-handed, standing next to hardware. Buttons opt in through
   min-height rather than fixed height so a wrapped label still fits. */
:root { --minhit: 36px; }
@media (pointer: coarse), (max-width: 640px) {
  :root { --minhit: 44px; }
}

button, .btn {
  font: inherit; font-size: 13px;
  min-height: var(--minhit);
  padding: 6px 12px; border-radius: 5px;
  border: 1px solid var(--line); background: transparent; color: var(--fg);
  cursor: pointer; transition: border-color 80ms, background 80ms;
  /* A double-tap on an adjacent control should not zoom the page. */
  touch-action: manipulation;
}
button:hover:not(:disabled) { border-color: var(--accent); }
button:active:not(:disabled) { background: color-mix(in srgb, var(--accent) 14%, transparent); }
button:disabled { opacity: 0.4; cursor: not-allowed; }
button.primary {
  background: var(--accent); border-color: var(--accent); color: var(--accent-fg);
  font-weight: 500;
}
button.danger { color: var(--danger); }
/* `.small` is for dense contexts (table rows, inline chips). It still keeps a
   real tap target on touch — only the text shrinks. */
button.small {
  padding: 3px 9px; font-size: 12px;
  min-height: calc(var(--minhit) - 8px);
}
@media (pointer: coarse), (max-width: 640px) {
  button.small { min-height: var(--minhit); padding: 6px 12px; font-size: 13px; }
}

input[type=file] { display: none; }

/* --- IconCycle -------------------------------------------------
   A stack of related glyphs playing as a loop. All frames are rendered and
   only opacity moves, so there is no JS timer running behind a page that is
   often mid-transfer. See js/components/IconCycle.js. */
.icon-cycle { position: relative; display: inline-block; vertical-align: middle; }
.icon-cycle .icon { position: absolute; inset: 0; opacity: 0; }
/* At rest the complete glyph shows — a stopped scanner should look like an
   idle radio, not like a signal frozen at one bar. */
.icon-cycle .icon:last-child { opacity: 1; }

.icon-cycle.playing .icon {
  opacity: 0;
  animation-iteration-count: infinite;
  /* steps(1) so each frame is held and then replaced. Cross-fading two
     line-art glyphs at 20px just looks like a smudge. */
  animation-timing-function: steps(1, end);
  animation-delay: calc(var(--i) * var(--cycle-step));
}
/* One keyframe set per frame count: a custom property cannot set a keyframe
   percentage, so the visible fraction has to be written out. Only these two
   lengths are used; any other renders static, which is the right failure. */
.icon-cycle.cycle-2.playing { --cycle-step: 0.45s; }
.icon-cycle.cycle-2.playing .icon { animation-name: cyc2; animation-duration: 0.9s; }
@keyframes cyc2 { 0%, 49.9% { opacity: 1; } 50%, 100% { opacity: 0; } }

.icon-cycle.cycle-3.playing { --cycle-step: 0.4s; }
.icon-cycle.cycle-3.playing .icon { animation-name: cyc3; animation-duration: 1.2s; }
@keyframes cyc3 { 0%, 33.2% { opacity: 1; } 33.3%, 100% { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  .icon-cycle.playing .icon { animation: none; opacity: 0; }
  .icon-cycle.playing .icon:last-child { opacity: 1; }
}
