/* Header, toolbar, listing, footer, log, progress bar, drop overlay. */

/* --- status bar / header -------------------------------------- */
header {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 14px; border-bottom: 1px solid var(--line);
  background: var(--card); flex-wrap: wrap;
}
header .title {
  font-size: 12px; font-weight: 600; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--muted);
}
header .device { font-family: var(--mono); font-size: 13px; }
/* The updater's own battery. Colour comes from the band class the component
   sets, never from --accent: see tokens.css at --ok for why a status colour
   cannot be a user preference. */
header .battery {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--mono);
  font-size: 12px;
  /* Both the SVG (currentColor) and the percentage take this. */
  color: var(--ok);
}
header .battery.warn   { color: var(--warn); }
header .battery.danger { color: var(--danger); }
header .battery .icon { display: block; }
/* Tabular figures: the number changes under a poll and a proportional font
   would shift the text beside it every time it crossed 100 -> 99. */
header .battery .pct { font-variant-numeric: tabular-nums; }

header .grow { flex: 1; }
/* Was an abstract dot with a box-shadow ring pulsing off it. It is a
   bluetooth glyph now, so the pulse had to move onto the glyph — a ring
   expanding out of a piece of line art reads as a rendering artefact rather
   than as a heartbeat. Opacity instead, which works on any shape. */
header .dot-icon {
  color: var(--muted); margin-right: 6px; vertical-align: middle;
}
header.connected .dot-icon {
  color: var(--accent);
  animation: pulse 2.2s ease-in-out infinite;
}
@keyframes pulse {
  50% { opacity: 0.35; }
}
@media (prefers-reduced-motion: reduce) {
  header.connected .dot-icon { animation: none; }
}

/* The header's action carries a glyph as well as its word. It is one toggle
   now rather than a Connect/Disconnect pair — see AppHeader.js — so the glyph
   is doing more work than it was: it is the part that changes shape when the
   label changes word, which is what keeps the swap from reading as the same
   button with different text. */
header button .icon { margin-right: 6px; vertical-align: -3px; }
/* inline-flex so the glyph centres on the word rather than sitting on its
   baseline, and so the button does not resize between "Connect" and
   "Connecting…" enough to move under a thumb already on its way down. */
header .conn {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 8.5em;
}

/* A glyph beside a word, anywhere. Buttons are inline-flex so the two centre
   on each other rather than sitting on a shared baseline, which is what made
   a 16px icon ride high next to 13px text. */
td.actions button:has(.icon),
.flash-step button:has(.icon) {
  display: inline-flex; align-items: center; gap: 5px;
}

/* --- appearance controls (ThemePicker) ------------------------- */
.theme-ctl { display: flex; align-items: center; gap: 2px; position: relative; }

/* Square icon buttons. base.css floors every button at --minhit for a thumb;
   these keep that as the *target* and just lose the horizontal padding, so
   they read as glyphs rather than as two more labelled actions. */
.icon-only {
  padding: 0; width: var(--minhit);
  display: inline-flex; align-items: center; justify-content: center;
  border-color: transparent; color: var(--muted);
}
.icon-only:hover:not(:disabled) { border-color: var(--line); color: var(--fg); }
.icon-only[aria-expanded="true"] { border-color: var(--accent); color: var(--fg); }
/* Delete is destructive and appears in every row. Red at rest would make an
   ordinary file listing look like an error report, so it stays muted until
   the pointer is on it and the intent is real. */
.icon-only.danger { color: var(--muted); }
.icon-only.danger:hover:not(:disabled) {
  color: var(--danger); border-color: var(--danger);
}

/* The palette menu. Anchored to .theme-ctl, right-aligned so it opens inward
   from the header's right-hand side rather than off the edge.

   Which is only true while the control *is* on the right — see the narrow
   override below. */
.theme-menu {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 40;
  min-width: 168px; max-width: calc(100vw - 24px); padding: 4px;
  background: var(--card); border: 1px solid var(--line); border-radius: 8px;
  box-shadow: 0 6px 22px rgba(0, 0, 0, 0.22);
  display: flex; flex-direction: column; gap: 1px;
}
.theme-opt {
  display: flex; align-items: center; gap: 9px;
  width: 100%; text-align: left; justify-content: flex-start;
  border-color: transparent; border-radius: 5px; font-size: 13px;
}
.theme-opt:hover:not(:disabled) { background: var(--bg); border-color: transparent; }
.theme-opt.on { color: var(--fg); font-weight: 500; }
.theme-opt .opt-label { flex: 1; }
/* A ring rather than a tick: a glyph inside 14px of arbitrary colour is
   unreadable on at least one of the seven. */
.theme-opt.on .swatch { box-shadow: 0 0 0 2px var(--card), 0 0 0 3px var(--fg); }

/* Not a <button>, so base.css's --minhit floor does not apply and it can be a
   real circle. The one in the menu row is decoration; the row is the target. */
.theme-ctl .swatch {
  --sw: var(--accent);
  width: 14px; height: 14px; flex: none;
  border-radius: 50%;
  background: var(--sw);
  border: 1px solid color-mix(in srgb, var(--fg) 22%, transparent);
}

/* On a narrow screen the header wraps, and the appearance controls land at the
   *left* of the second row. Right-aligning a 168px menu to a ~92px control
   there puts most of it off the left edge of the screen — observed on a phone
   as labels cut in half with their swatches missing entirely, which reads as a
   broken menu rather than as one that is merely positioned badly. Open it from
   the left edge of the control instead, where the space is.

   Widths between the wrap point and 640px get this too, and are fine either
   way: a 168px menu opening rightward from a control at x=420 still ends well
   inside the viewport. The max-width above is the backstop for anything
   narrower than the menu itself. */
@media (max-width: 640px) {
  /* The header wraps here, so make it wrap *predictably*. .grow becomes a
     full-width spacer, which forces the break at one known place: line 1 is
     always the title and the device, line 2 is always the controls. Left to
     itself the break lands wherever the device name happens to end, so a long
     name could push the appearance controls onto a line of their own and the
     action onto a third. */
  header .grow { flex-basis: 100%; }

  /* Appearance at one end of that line, the action at the other.
     Both halves matter. The action keeps the edge it occupies on a desktop —
     it is the one control a thumb reaches for repeatedly, and a button that
     moves to the opposite side of the screen because the window got narrower
     is a button you have to look for. And the appearance controls keep the
     left, away from it: they are pressed once ever, and the cost of a mis-tap
     between "connect" and "change the accent colour" is all on one side. */
  header .conn { margin-left: auto; }

  .theme-menu { left: 0; right: auto; }
}

/* --- toolbar --------------------------------------------------- */
.toolbar {
  display: flex; gap: 6px; padding: 8px 14px;
  border-bottom: 1px solid var(--line); align-items: center; flex-wrap: wrap;
}
.toolbar .grow { flex: 1; }

/* Icon + label. Icon-only was compact and unreadable — five unlabelled glyphs
 * is a memory test, and the 32px squares were below a usable tap target. */
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
}
.icon-btn .icon { display: block; flex: none; }
.icon-btn .label { white-space: nowrap; }
/* The glyph follows the button's text colour, including the disabled state,
 * because `fill: currentColor` is set on the path. */
.icon-btn:disabled .icon { opacity: 0.5; }

/* Separates the one action that works offline (Flash updater) from the ones
 * that need a connection. Purely visual — the disabled states already carry
 * the meaning. */
.tb-sep {
  width: 1px; align-self: stretch; margin: 2px 4px;
  background: var(--line); flex: none;
}

/* On a narrow screen the toolbar becomes a two-row grid of equal buttons
 * rather than a ragged wrap: predictable target positions matter more than
 * compactness when you are tapping one-handed. */
@media (max-width: 640px) {
  .toolbar { gap: 8px; padding: 10px 12px; }
  .toolbar .icon-btn { flex: 1 1 calc(50% - 8px); justify-content: flex-start; }
  .toolbar .grow, .toolbar .tb-sep { display: none; }
}

/* --- DFU banner ------------------------------------------------
 *
 * Sits between the toolbar and the listing, and is the only thing on screen
 * that reports work happening on the *target* rather than on this device.
 * Loud on purpose: a transfer runs for half a minute or more with no other
 * visible sign, and the failure it exists to prevent is someone assuming a
 * silent app means a broken device.
 */
.dfu-banner {
  padding: 10px 14px; border-bottom: 1px solid var(--line);
  background: color-mix(in srgb, var(--accent) 8%, var(--bg));
  display: flex; flex-direction: column; gap: 7px;
  font-size: 13px;
}
.dfu-banner.ok   { background: color-mix(in srgb, var(--accent) 12%, var(--bg)); }
.dfu-banner.fail { background: color-mix(in srgb, var(--danger) 12%, var(--bg)); }

.dfu-row { display: flex; align-items: center; gap: 10px; }
.dfu-row .grow { flex: 1; }
.dfu-state { font-weight: 600; }
.dfu-subject, .dfu-detail, .dfu-result {
  font-family: var(--mono); font-size: 11px; color: var(--muted);
}
.dfu-result { color: var(--fg); }
.dfu-banner.fail .dfu-result { color: var(--danger); }
.dfu-pct { font-family: var(--mono); font-size: 11px; color: var(--fg); }

/* The same pulsing dot the header uses for "connected", reused here for
 * "working" — one visual vocabulary for liveness. */
.dfu-dot {
  width: 8px; height: 8px; border-radius: 50%; flex: none;
  background: var(--accent);
}
.dfu-banner.run .dfu-dot { animation: dfu-pulse 1.2s ease-in-out infinite; }
.dfu-banner.fail .dfu-dot { background: var(--danger); }
@keyframes dfu-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.25; }
}

.dfu-bar {
  height: 4px; border-radius: 2px; overflow: hidden;
  background: color-mix(in srgb, var(--fg) 12%, transparent);
}
.dfu-bar .fill {
  height: 100%; background: var(--accent);
  transition: width 200ms linear;
}
.dfu-banner.fail .dfu-bar .fill { background: var(--danger); }
/* Every step other than the upload has no percentage to show. A bar parked
 * at 0% through a six-second scan reads as stuck, so it sweeps instead. */
.dfu-bar.indeterminate .fill {
  width: 35% !important; animation: indet 1.4s linear infinite;
}

.dfu-watch { font-size: 12px; padding: 4px 10px; }

/* Stop is a destructive-ish action sitting next to a neutral one, so it is
 * tinted rather than left identical to "Log" — but not filled red: it is the
 * button you want an anxious operator to press without hesitating, and the
 * cost of pressing it by mistake is one re-trigger. */
.dfu-stop { font-size: 12px; padding: 4px 10px; }
.dfu-stop:not(:disabled) {
  border-color: color-mix(in srgb, var(--danger) 55%, transparent);
  color: var(--danger);
}
.dfu-stop:not(:disabled):hover {
  background: color-mix(in srgb, var(--danger) 12%, transparent);
}
.dfu-dismiss {
  background: none; border: 0; color: var(--muted);
  font-size: 18px; line-height: 1; padding: 0 4px; cursor: pointer;
}
.dfu-dismiss:hover { color: var(--fg); }

/* A control worth looking at right now. Used on the toolbar's Log button
 * while a DFU is running — the banner can be dismissed, this cannot. */
.icon-btn.attention:not(:disabled) {
  border-color: var(--accent);
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  .dfu-banner.run .dfu-dot { animation: none; }
  .dfu-bar.indeterminate .fill { animation: none; width: 100% !important; }
}

@media (max-width: 640px) {
  /* The label is the first thing to go: the icon plus the banner it sits in
   * already say what the button opens. */
  .dfu-watch .label { display: none; }
  .dfu-stop .label { display: none; }
  .dfu-subject { display: none; }
}

/* --- listing table ---------------------------------------------
 *
 * Every rule below is scoped to .file-table, and that is not tidiness.
 * These started as bare `table` / `thead` / `tbody td` selectors, which was
 * true right up until a second table existed. The scanner's arrived and
 * inherited all of it — most damagingly `tbody tr { display: grid }` from the
 * narrow-screen block, which turns every cell into its own full-width row.
 * On a phone the scanner rendered as a stack of one-cell rows with a header
 * reading only "SIGNAL" (thead th:nth-child(2) is hidden here, and the
 * scanner hides its own third), inconsistent borders, and most of the width
 * unused. Nothing in the scanner's own stylesheet was wrong.
 */
main { flex: 1; overflow: auto; }
.file-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.file-table thead th {
  text-align: left;
  padding: 8px 14px 6px;
  color: var(--muted); font-weight: 500; font-size: 11px;
  text-transform: uppercase; letter-spacing: 0.1em;
  background: var(--bg); border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 1;
}
.file-table tbody td {
  padding: 9px 14px;
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}
.file-table tbody tr { position: relative; }
.file-table tbody tr:hover td { background: var(--card); }

/* Left stripe encodes entry type — dirs get an accent stripe, files stay quiet. */
.file-table tbody td.name {
  font-family: var(--mono); cursor: pointer; user-select: none;
  padding-left: 22px; position: relative;
}
.file-table tbody td.name::before {
  content: ""; position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
  width: 4px; height: 16px; border-radius: 2px;
  background: var(--stripe-file);
}
.file-table tbody td.name.dir::before { background: var(--stripe-dir); }
.file-table tbody td.name.dir::after  { content: "/"; color: var(--muted); }

/* The live config.txt opens the editor rather than downloading, so it gets its
   own stripe — same language as the dir stripe, different meaning. */
.file-table tbody td.name.cfg::before { background: var(--warn); }
/* Log files open the viewer. Same idea again: a stripe means "this row does
   something other than download". */
.file-table tbody td.name.log::before { background: var(--muted); }

.file-table td.size, .file-table td.actions {
  color: var(--muted); font-variant-numeric: tabular-nums; font-size: 12px;
}
/* Mono on the size and NOT on the actions. base.css gives every button
   `font: inherit`, so mono on the actions cell reached the buttons inside it
   — which is why "flash" and "check" were the only monospace button labels in
   the app, next to a scanner whose Flash button was not. The size column
   wants the mono face for its own reason: digits that line up between rows. */
.file-table td.size { font-family: var(--mono); }
.file-table td.actions { text-align: right; white-space: nowrap; }
.file-table td.actions button { margin-left: 5px; }

/* A phone cannot show name + size + three actions on one line without making
   all of them unhittable. Drop the size column, let the row breathe, and give
   the actions a line of their own. */
@media (max-width: 640px) {
  .file-table { font-size: 14px; }
  .file-table thead th:nth-child(2), .file-table tbody td.size { display: none; }

  /* The header cells give up their horizontal padding here, and the label
     gets it back as a text-indent.
     This looks like fussiness and is not. `tbody tr` below is display:grid,
     which takes the row out of table layout — and a row that is no longer a
     table-row is laid out against the table's *columns*, whose width the
     still-table-formatted header cells decide. Their 14px of padding on each
     side therefore came off every body row: the actions cell ended 28px short
     of the table, and the buttons inside it sat 40px from the edge of the
     screen against a 22px inset on the left. Measured in a real engine, at
     360/390/430; it is 12px now, and the header label lines up with the file
     names instead of sitting 8px left of them. text-indent because it moves
     the text without touching the box. */
  .file-table thead th { padding-left: 0; padding-right: 0; }
  .file-table thead th:first-child { text-indent: 22px; }
  .file-table tbody td { padding: 10px 12px; }
  .file-table tbody td.name { padding-left: 22px; }
  .file-table tbody tr { display: grid; grid-template-columns: 1fr; }
  /* Right, not left. The actions get a line of their own here, and left-
     aligned they sit directly under the file name — so the row reads as two
     lines of the same sentence and the tap targets are hardest to reach
     one-handed. Against the right edge they read as controls rather than as
     continued text, and they land under the thumb. */
  .file-table tbody td.actions {
    text-align: right; padding-top: 0; padding-bottom: 12px;
    border-bottom: 1px solid var(--line);
  }
  .file-table tbody td.name { border-bottom: none; }
  .file-table td.actions button { margin: 0 0 0 6px; }
}

.empty {
  text-align: center; padding: 60px 20px; color: var(--muted);
  font-family: var(--mono);
}

/* --- footer + log ---------------------------------------------- */
footer {
  padding: 6px 14px; border-top: 1px solid var(--line);
  display: flex; justify-content: space-between; align-items: center;
  background: var(--card); font-size: 12px; color: var(--muted);
  font-family: var(--mono);
}
#log {
  max-height: 18vh;
  overscroll-behavior: contain; overflow: auto; padding: 8px 14px; margin: 0;
  border-top: 1px solid var(--line); background: var(--bg);
  font-family: var(--mono); font-size: 11px; color: var(--muted);
  white-space: pre-wrap; line-height: 1.55;
}
#log .err  { color: var(--danger); }
#log .ok   { color: var(--accent); }
/* Something that probably worked but couldn't be confirmed — a reset that
   drops the link before it can answer looks identical to a timeout. */
#log .warn { color: var(--warn); }
#log .ts   { color: var(--muted); opacity: 0.6; margin-right: 6px; }

/* --- top-of-viewport progress bar (YouTube style) -------------- */
#progress {
  position: fixed; top: 0; left: 0; right: 0; height: 2px;
  background: transparent; z-index: 10;
  pointer-events: none;
}
#progress .fill {
  height: 100%; width: 0%; background: var(--accent);
  transition: width 120ms linear;
  box-shadow: 0 0 6px color-mix(in srgb, var(--accent) 70%, transparent);
}
#progress.indeterminate .fill {
  width: 30%; animation: indet 1s linear infinite;
}
@keyframes indet {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(400%); }
}
#progress-label {
  position: fixed; top: 10px; right: 14px; z-index: 10;
  font-family: var(--mono); font-size: 11px; color: var(--muted);
  background: var(--card); border: 1px solid var(--line);
  padding: 3px 8px; border-radius: 4px;
  display: none;
}
#progress-label.shown { display: block; }

/* --- drag-and-drop overlay ------------------------------------- */
#drop-overlay {
  position: fixed; inset: 0; z-index: 20;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(3px);
  display: none;
  pointer-events: none;
}
#drop-overlay.shown { display: block; }
#drop-overlay .frame {
  position: absolute; inset: 24px;
  border: 2px dashed var(--accent);
  border-radius: 12px;
  display: grid; place-items: center;
  color: var(--fg);
}
#drop-overlay .frame-inner { text-align: center; }
#drop-overlay .frame-title { font-size: 20px; font-weight: 500; margin: 0 0 6px; }
#drop-overlay .frame-sub {
  font-family: var(--mono); color: var(--muted); font-size: 13px;
}
#drop-overlay.reject .frame { border-color: var(--danger); }
#drop-overlay.reject .frame-sub { color: var(--danger); }

/* Offer, not an interruption — a waiting service-worker update must never
   pull the page out from under a transfer in progress. */
/* Offered, not demanded: this is a convenience, and it sits next to the one
   control people came for. Accent-bordered rather than filled — a solid
   button here would compete with Connect, which is the actual job. */
header .install {
  border-color: var(--accent); color: var(--accent); font-size: 12px;
}
header .install:hover { background: color-mix(in srgb, var(--accent) 14%, transparent); }

header .update {
  border-color: var(--warn); color: var(--warn); font-size: 12px;
}
header .update:hover { border-color: var(--warn); background: color-mix(in srgb, var(--warn) 12%, transparent); }

/* The device's verdict on a firmware file, under its name in the listing.
 * Its own line rather than a column: it is absent for most rows, and an empty
 * column for every folder and log file is worse than a line that appears when
 * there is something to say. */
.file-verdict     { font-size: 11px; opacity: .75; margin-top: 2px; }
.file-verdict.ok  { color: var(--accent); }
.file-verdict.err { color: var(--danger); opacity: 1; }
.file-verdict.warn{ color: var(--warn); }
