/* Shared components. Linked after tokens.css and before any page sheet, so a
 * page can still override a component but never has to redefine one.
 *
 * A selector defined here is defined nowhere else -- that is the rule
 * tests/test_stylesheet.py enforces, and it is the permanent replacement for
 * "keep these in sync by hand" comments.
 */

/* Full-screen modal backdrop.
 *
 * Had five definitions. Three were identical (import_instances, rescue_images,
 * workers/workspace), dirty_harry carried a stripped-down base under its own
 * .dirty-harry-overlay modifier, and the source-cloud auth partial disagreed:
 * centered rather than top-anchored, a darker backdrop, and z-index 80.
 * import.html and workers/workspace.html include both families, so which one
 * rendered was decided by include order -- undocumented and load-bearing.
 *
 * The page variant wins. It is the majority, it is already what operators see
 * on import and workers, flex-start plus overflow:auto handles a form taller
 * than the viewport, and z-55 slots into the existing scale (wizard 40,
 * picker 50, log and form 55, help 58-61). The partial's z-80 wrongly stacked
 * its modal above the help overlay system.
 */
.form-overlay {
  position: fixed;
  inset: 0;
  background: rgba(14, 11, 12, 0.38);
  display: none;
  align-items: flex-start;
  justify-content: center;
  overflow: auto;
  padding: 40px 24px;
  z-index: 55;
}

/* Both activation classes, deliberately. The import and workers scripts toggle
 * .open; the source-cloud auth partial toggles .active. Honouring only one
 * would silently break every modal in the other family, which is the most
 * likely functional break in this phase -- so no classList call anywhere has
 * to change.
 */
.form-overlay.open,
.form-overlay.active {
  display: flex;
}

/* The card inside the overlay, and its three bands.
 *
 * Only the backdrop lived here; the frame itself was defined four times over
 * in page stylesheets (import_instances, rescue_images, source_cloud_auth,
 * migration_plans). Any page that opened an overlay without one of those
 * loaded got the dim backdrop with nothing drawn on it -- a close button and
 * some text floating on the page, which is what the migration board showed
 * the first time it opened the shared start modal.
 *
 * Those page copies still load after this file and still win where they
 * differ; these are the same values, so nothing that already worked moves.
 */
.form-frame {
  width: min(720px, 100%);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-modal);
}
.form-head {
  display: flex;
  align-items: baseline;
  gap: 14px;
  padding: 18px 18px 14px;
  border-bottom: 1px solid var(--line);
  background: var(--surface-2);
  border-radius: var(--radius) var(--radius) 0 0;
}
.form-head h3 {
  margin: 0;
  font-size: 16px;
  color: var(--ink);
}
.form-head .sub {
  font-size: 12px;
  color: var(--dim);
}
.form-head .right {
  margin-left: auto;
}
.form-body {
  padding: 18px;
}
.form-actions {
  display: flex;
  gap: 10px;
  justify-content: flex-end;
  padding: 14px 18px 18px;
  border-top: 1px solid var(--line);
  background: var(--surface-2);
  border-radius: 0 0 var(--radius) var(--radius);
}

/* Buttons.
 *
 * .btn is worn by both <button> and <a> elements. base.css styles the bare
 * `button` element, which is why login, the migration cards and the migrate
 * toolbar each grew their own .btn -- an anchor gets nothing from a `button`
 * rule. Geometry here is base's, so a <button class="btn"> looks the same
 * either way.
 *
 * The focus ring is the point. A `button:focus-visible` rule cannot match
 * <a class="btn">, so every anchor-styled button on the site was keyboard-
 * invisible: you could tab to it with no indication of where you were. That
 * is the accessibility bug the plan describes, though not quite where it
 * looked -- operator_app.css and run.css do have their own button
 * :focus-visible rules, so the gap is anchors, not those pages.
 *
 * The two scoped variants (.migration-actions .btn, .workspace-toolbar .btn)
 * are deliberately different geometry and outrank this on specificity; they
 * keep their looks and gain the ring, because they set no focus properties of
 * their own.
 */
.btn {
  display: inline-block;
  appearance: none;
  cursor: pointer;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--body);
  padding: 7px 14px;
  font-family:
    Inter,
    system-ui,
    -apple-system,
    sans-serif;
  font-size: 12.5px;
  font-weight: 500;
  text-decoration: none;
  transition: background 100ms ease;
}

.btn:hover {
  background: var(--surface-2);
}

.btn.primary {
  background: var(--red);
  border-color: var(--red);
  color: #fff;
}

.btn.primary:hover {
  background: var(--red-dark);
}

.btn:disabled {
  cursor: not-allowed;
  opacity: 0.52;
}

.btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px rgba(207, 21, 66, 0.35);
}

/* Status chip.
 *
 * Conservative on purpose: only the byte-identical pair (the conversion
 * workspace and the migrations board) merges here, with its token-named
 * modifiers. The three other geometries -- rescue_images, import and
 * object_migrate, which are smaller, uppercase and letter-spaced -- stay on
 * their pages and win on load order, because unifying them would be a visual
 * change dressed as a refactor. They migrate opportunistically, when someone
 * is in that file anyway.
 *
 * Modifiers are named for tokens (.green/.amber/.red/.blue), not for meanings
 * (.chip-warn), so a chip's colour and the token it comes from cannot drift
 * apart.
 */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 11.5px;
  font-weight: 500;
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--gray-soft);
  color: var(--dim);
  white-space: nowrap;
}

.chip.green {
  background: var(--green-soft);
  color: var(--green);
}

.chip.amber {
  background: var(--amber-soft);
  color: var(--amber);
}

.chip.red {
  background: var(--red-soft);
  color: var(--red-dark);
}

.chip.blue {
  background: var(--blue-soft);
  color: var(--blue);
}

/* Dense table header.
 *
 * Three copies (the conversion workspace, the migrations board and
 * migration plans) become one, opted into by class. The plan expected to add a .table-dense
 * class to three tables' markup; no markup change is needed, because all three
 * already share .tbl -- so this is a pure deletion and DOM classes are
 * untouched.
 *
 * The selector is .table-dense, not .tbl, and that matters. Consolidating on
 * .tbl leaked the dense header into migration_plans' modal tables, which wear
 * .tbl but were never meant to be dense -- the page had scoped its copy to
 * .migration-content precisely to avoid that. The visual baseline caught it.
 * An explicit opt-in class cannot leak.
 *
 * base.css's `th` stays canonical for everything else; this differs from it
 * only in padding (9px vs 10px) and nowrap. The JetBrains Mono headers on the
 * run console and the migrate inline table are deliberately their own and are
 * left alone.
 */
.table-dense th {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--dim);
  text-align: left;
  padding: 9px 16px;
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

/* Site chrome: the black bar every page wears.
 *
 * Lived in base.css and was hand-copied into the two help pages, and the run
 * console reinvented it under .app-header/.logo at a different height with a
 * different border. It is a component, so it lives here and every page --
 * including Family B, which never linked base.css -- gets the same one.
 */

.topbar {
  height: 48px;
  background: var(--ink);
  color: #fff;
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 0 24px;
  border-bottom: 3px solid var(--red);
  font-size: 12px;
}

.brand {
  display: flex;
  align-items: center;
  gap: 10px;
}

.brand .mark {
  width: 7px;
  height: 20px;
  background: var(--red);
  border-radius: 1px;
}

.brand .name {
  font-weight: 700;
  font-size: 14px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.brand .sub {
  font-size: 11px;
  color: #9b938e;
  padding-left: 12px;
  border-left: 1px solid #3a3335;
}

.topbar .context {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 18px;
  font-size: 12px;
  color: #c9c2bd;
}

.topbar .context a {
  color: #c9c2bd;
  text-decoration: none;
}

.topbar .context a:hover {
  color: #fff;
}

.topbar .context b {
  color: #fff;
  font-weight: 600;
}

.topbar .context .sep {
  color: #4a4346;
}

.topbar .context button {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: transparent;
  border: 1px solid #4a4346;
  border-radius: var(--radius-sm);
  color: #c9c2bd;
  font-family:
    Inter,
    system-ui,
    -apple-system,
    sans-serif;
  padding: 5px 12px;
  font-size: 12px;
  cursor: pointer;
}

.topbar .context button:hover {
  color: #fff;
  border-color: #9b938e;
  background: transparent;
}

/* Topbar "N imports running" pill, written by active_imports.js. Lives on the
 * dark topbar, so it takes its own light-on-dark treatment rather than the
 * .chip palette, which is tuned for the light surfaces. */
.topbar .context a.active-imports {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--dot-amber);
  border-radius: 999px;
  padding: 2px 10px;
  font-size: 11px;
  font-weight: 500;
  color: var(--dot-amber);
  text-decoration: none;
}

.topbar .context a.active-imports::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--dot-amber);
}

.topbar .context a.active-imports:hover {
  color: var(--surface);
  border-color: var(--surface);
}

.topbar .context a.active-imports[hidden] {
  display: none;
}

/* Small inline "working on it" ring, at text scale. Written by setCountBadge()
 * in lib/dom.js for a tab-count badge, and by the import start modal for a
 * disk whose job the server has not created yet.
 *
 * Same ring idiom as .browser-spinner, but its keyframes live here rather
 * than in base.css: components.css is linked on pages that do not load
 * base.css, and a missing animation degrades to a motionless ring that reads
 * as a rendering bug. */
.mini-spinner {
  display: inline-block;
  width: 9px;
  height: 9px;
  border: 1.5px solid var(--line-strong);
  border-top-color: var(--red);
  border-radius: 50%;
  animation: mini-spin 0.8s linear infinite;
}

@keyframes mini-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .mini-spinner {
    animation-duration: 2.4s;
  }
}

/* Progress rows for a start that takes a while: one row per unit of work,
   each resolving from the server's own record rather than from the request we
   sent. Worn by the import page's start modal (a row per disk) and the
   workers page's add-worker modal (a row per boot phase), which is why it
   lives here rather than on either page's sheet. */
.start-rows {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
/* The question, asked in the same frame that then reports the answer. */
.start-confirm {
  font-size: 13px;
  line-height: 1.55;
  color: var(--body);
}
.start-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 9px 4px;
  border-bottom: 1px solid var(--line);
}
.start-row:last-child {
  border-bottom: 0;
}
.start-mark {
  flex: 0 0 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 19px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--dim);
}
.start-row.started .start-mark {
  color: var(--green);
}
.start-row.failed .start-mark {
  color: var(--red);
}
.start-text {
  flex: 1 1 auto;
  min-width: 0;
}
.start-label {
  font-size: 12.5px;
  color: var(--ink);
}
.start-detail {
  font-size: 11px;
  color: var(--dim);
  margin-top: 2px;
}
.start-row.failed .start-detail {
  color: var(--red-dark);
}
.start-job {
  flex: 0 0 auto;
  font-size: 10.5px;
  color: var(--dim);
  align-self: center;
}
.start-summary {
  font-family: "JetBrains Mono", monospace;
  font-size: 10.5px;
  letter-spacing: 0.04em;
  color: var(--dim);
}

.form-head.stacked {
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
}
.form-head.stacked .form-head-line {
  display: flex;
  align-items: baseline;
  gap: 14px;
}
.form-head.stacked .form-head-line .right {
  margin-left: auto;
}

/* Loading skeleton for a table body. A static "Loading..." line in a wide
   table reads as a hung page; a shimmer visibly keeps working while a slow
   upstream listing comes back. Currently used by the Workloads table on
   Convert (portal.js renderTableSkeleton).

   The sweep is a background-position animation over a three-stop gradient,
   so it costs one compositor-friendly property and no layout. Under
   prefers-reduced-motion the sweep stops and the bars stay as flat blocks --
   still visibly placeholders, just not moving. */
.skeleton-row td {
  padding: 13px 14px;
}
.skeleton-bar {
  display: block;
  height: 10px;
  border-radius: 3px;
  /* --line, not --gray-soft: the sweep highlight is near-white, so the base
     has to be dark enough that the travelling band is actually legible.
     A softer base looked static, which is the one thing this must not do. */
  background-color: var(--line);
  background-image: linear-gradient(
    90deg,
    var(--line) 0%,
    var(--line) 35%,
    var(--surface-2) 50%,
    var(--line) 65%,
    var(--line) 100%
  );
  background-size: 250% 100%;
  background-repeat: no-repeat;
  animation: skeleton-sweep 1.4s linear infinite;
}
@keyframes skeleton-sweep {
  from {
    background-position: 125% 0;
  }
  to {
    background-position: -25% 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  .skeleton-bar {
    animation: none;
    background-image: none;
  }
}

/* Phone-width topbar. The cloud/region/project context and sign-out stay on
   the 48px bar and scroll sideways inside it rather than pushing the page
   wider; the brand sub-label goes to make room. Desktop is untouched. */
@media (max-width: 600px) {
  .topbar {
    padding: 0 12px;
    gap: 12px;
  }
  .brand .sub {
    display: none;
  }
  .topbar .context {
    gap: 10px;
    min-width: 0;
    overflow-x: auto;
    white-space: nowrap;
  }
}
