/*
 * Shared chrome.
 *
 * Colours are custom properties so a theme change is a `data-theme` swap, not
 * a reload — Element pushes theme changes at runtime. Values approximate
 * Element's Compound tokens so widgets look native in the right panel.
 */

:root {
  --bg: #ffffff;
  --bg-subtle: #f4f6fa;
  --fg: #1b1d22;
  --fg-muted: #737d8c;
  --border: #e3e8f0;
  --accent: #0dbd8b;
  --danger: #d51928;
  --radius: 8px;
  --radius-sm: 6px;
  --gap: 12px;
  /* A focus ring that is visible on both themes, and never the only signal:
     every control here also changes colour when it is on. */
  --focus: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent);
  --font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
}

[data-theme="dark"] {
  --bg: #16191e;
  --bg-subtle: #21262c;
  --fg: #edf3ff;
  --fg-muted: #a9b2bc;
  --border: #34383d;
  --accent: #0bc491;
  --danger: #ff5b55;
}

* { box-sizing: border-box; }

html, body { margin: 0; height: 100%; }

body.app {
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.45;
  color: var(--fg);
  background: var(--bg);
  /* The right panel is ~320-400px wide; never assume more. */
  overflow-x: hidden;
}

.root, .page {
  display: flex;
  flex-direction: column;
  padding: var(--gap);
  gap: var(--gap);
}

.page { min-height: 100%; max-width: 720px; margin: 0 auto; }

/* The widget is the document, and the document scrolls.
 *
 * A definite height here would make the panel compress its columns instead:
 * auto-height content in a fixed-height flex column overflows, and it overflows
 * *visibly*, which is how a search result list ends up painted across the queue
 * below it. The two-column layout below is the only shape that wants a fixed
 * height, and it asks for one itself. */
body.widget .root { min-height: 0; overflow-y: auto; }

/* Fill the frame rather than ending wherever the content stops: a panel that
   stops short in a tall sidebar leaves the queue a scroll area with nothing to
   scroll, and a bordered box that does not reach the edge reads as unfinished.
   `flex: 1 0 auto` grows and never shrinks, so tall content still scrolls the
   document instead of being squashed into the frame. */
body.widget .root > .panel { flex: 1 0 auto; }

.muted { color: var(--fg-muted); }

code, pre { font-family: var(--mono); font-size: 12px; }

/* Boot ------------------------------------------------------------------ */

.boot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gap);
  padding: 32px 0;
  color: var(--fg-muted);
}

.spinner {
  width: 20px;
  height: 20px;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; }
}

.boot__text { margin: 0; }

/* Panels ---------------------------------------------------------------- */

.panel {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-subtle);
  padding: var(--gap);
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  min-height: 0;
}

.panel--error { border-color: var(--danger); }

.panel__title {
  margin: 0 0 var(--gap);
  font-size: 15px;
  overflow-wrap: anywhere;
}

.kv {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 4px var(--gap);
  margin: 0 0 var(--gap);
}

.kv dt { color: var(--fg-muted); }
.kv dd { margin: 0; overflow-wrap: anywhere; }

/* Live state ------------------------------------------------------------ */

.state {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 0;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--gap);
}

.state__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  flex: none;
}

.state__text { font-weight: 600; }
.state__meta { margin-left: auto; font-size: 12px; }

.btn {
  font: inherit;
  line-height: 1.2;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 6px 12px;
  cursor: pointer;
  /* Only the properties that change. A transition on `all` would animate the
     border colour of every button in a swapped-in list. */
  transition: border-color 120ms ease, background-color 120ms ease, color 120ms ease;
}

.btn:hover:not(:disabled) { border-color: var(--accent); }
.btn:active:not(:disabled) { transform: translateY(1px); }
/* Keyboard only: a ring on every click makes the panel look like it is
   flashing at you. */
.btn:focus-visible, .search__input:focus-visible, .search__kind:focus-visible {
  outline: none;
  box-shadow: var(--focus);
}

/* Catalogue ------------------------------------------------------------- */

.page__head h1 { margin: 0 0 4px; font-size: 20px; }
.page__head p { margin: 0; }

.cards { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--gap); }

.card {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--gap);
}

.card__title { margin: 0 0 4px; font-size: 16px; display: flex; align-items: center; gap: 8px; }
.card__desc { margin: 0 0 var(--gap); color: var(--fg-muted); }
.card__caps { margin-bottom: var(--gap); }
.card__caps ul { margin: 8px 0 0; padding-left: 20px; }

.card__add {
  margin: 0;
  padding: 8px;
  background: var(--bg-subtle);
  border-radius: var(--radius);
  overflow-x: auto;
}

.tag {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 2px 6px;
  border-radius: 4px;
  background: var(--accent);
  color: #fff;
}

.tag--muted { background: var(--border); color: var(--fg-muted); }

/* Media player ---------------------------------------------------------- */

.player { display: flex; flex-direction: column; gap: var(--gap); }

/* The two halves of the player swap independently: "now" when the track or
 * the transport changes, "controls" when something they draw changes (D23).
 * Neither is redrawn on a timer — the bar inside "now" is run by the browser
 * from a position and a clock (D27). Each is its own flex column so the gap
 * above is duplicated between them, not lost to a wrapper div. */
.player__live, .player__shell { display: flex; flex-direction: column; gap: var(--gap); }

.player__title { font-weight: 600; overflow-wrap: anywhere; }
.player__artist { font-size: 13px; }

.progress {
  height: 4px;
  background: var(--border);
  border-radius: 2px;
  overflow: hidden;
}

/* A seekable bar is a control: point at it like one, and make it tall enough
 * to actually hit. The attribute is the same signal the click handler reads,
 * so the styling cannot drift from the behaviour. */
.progress[data-seek-url] { cursor: pointer; height: 8px; border-radius: 4px; }

/* The fill is full width and scaled, not sized: a width would relayout on
 * every frame of a bar that runs for the length of a film, and a transform on
 * the compositor does not. progress.js owns the transform; the inline one the
 * template writes is the same value for a first paint with no script yet. */
.progress__fill {
  height: 100%;
  background: var(--accent);
  transform-origin: left center;
  transform: scaleX(0);
}

.player__controls { display: flex; gap: 8px; align-items: center; }

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

/* Stop throws away the room's queue with no undo, so it borrows the colour
 * the errors use rather than looking like a bigger pause. */
.btn--danger { color: var(--danger); border-color: var(--danger); }

/* A mode button that is on. The badges already use the accent as the "this is
 * set" colour, so the button carries it too rather than a badge repeating what
 * the button beside it says. */
.btn--active { background: var(--accent); border-color: var(--accent); color: var(--bg); }

/* The queue-head toggles are a size down from the transport: the same button
 * on a 13px line reads as a call to action rather than as a switch. */
.btn--small { padding: 2px 8px; font-size: 11px; }

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

.player__meta { font-size: 12px; margin: 0; }

.error { color: var(--danger); margin: 0; }

/* Queue ------------------------------------------------------------------
 *
 * Element hosts widgets in a narrow panel, so this is a one-column list that
 * degrades by dropping columns rather than wrapping: the title is what people
 * read, everything else is context.
 */

.queue {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: var(--gap);
  border-top: 1px solid var(--border);
  padding-top: var(--gap);
}

.queue__head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  font-size: 13px;
  color: var(--fg-muted);
}

.queue__badges { display: flex; flex-wrap: wrap; gap: 4px; margin-left: auto; }

.badge {
  font-size: 11px;
  line-height: 1.7;
  padding: 0 7px;
  border-radius: 999px;
  background: var(--border);
  color: var(--fg-muted);
  white-space: nowrap;
}

/* The accent is for the two states a room would otherwise have to be told
   about — party mode and a closed queue — not for every fact about a row. */
.badge--on { background: var(--accent); color: var(--bg); }

/* A film in a list of songs should not read as a very long song. Muted rather
 * than accented: it is a fact about the row, not a call to action, and the
 * accent colour is already doing the job of marking your own tracks. */
.badge--video {
  background: var(--border);
  color: var(--fg-muted);
  margin-left: 6px;
}

/* Whose turn it is. Shown because a fairness rule people can predict is one
 * they accept, and a rotation — unlike a play counter — can simply be drawn. */
.rotation {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 11px;
  color: var(--fg-muted);
}

.rotation__who {
  padding: 1px 6px;
  border: 1px solid var(--border);
  border-radius: 999px;
}

.rotation__who:first-child {
  border-color: var(--accent);
  color: var(--fg);
}

.queue__list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
}

.queue__item {
  display: grid;
  grid-template-columns: 1.6em 1.5em 1fr auto auto auto;
  align-items: center;
  gap: 8px;
  padding: 4px 2px;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
}

/* Whoever queued the row, as a picture. A background rather than an <img> so
 * somebody with no picture falls back to their initial instead of a broken
 * image; the 404 is cached by the avatar endpoint, so a re-render does not
 * refetch every row. */
.queue__avatar {
  width: 1.5em;
  height: 1.5em;
  border-radius: 50%;
  background-color: var(--border);
  background-size: cover;
  background-position: center;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 10px;
  font-weight: 600;
  color: var(--fg-muted);
  text-transform: uppercase;
  overflow: hidden;
}

/* Nobody named: a hole would shift the row, so the cell stays. */
.queue__avatar--unknown { background-color: transparent; }

.queue__item:last-child { border-bottom: none; }

/* Your own entries, marked because the remove button appears on them and
 * nowhere else unless you have the power level. */
.queue__item--mine .queue__pos { color: var(--accent); }

.queue__pos {
  color: var(--fg-muted);
  font-size: 11px;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.queue__track {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.queue__title,
.queue__artist,
.queue__who {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.queue__artist { font-size: 11px; }
.queue__who { font-size: 11px; max-width: 8ch; }

/* Durations are compared down a column, so they must not shimmy as the digits
 * change width. */
.tabular { font-variant-numeric: tabular-nums; }
.queue__len { font-size: 11px; }

.queue__actions { display: flex; gap: 2px; }

/* Pushed away from the badges it sits beside: the count and the mode markers
 * are a read, and the switch is a control. */
.btn--icon {
  padding: 2px 6px;
  min-width: 0;
  line-height: 1;
  font-size: 13px;
}

.queue__more { font-size: 11px; margin: 0; }

/* Media panel ------------------------------------------------------------
 *
 * Three regions — transport, search, queue — laid out against the panel's own
 * size. A widget in a chat sidebar is tall and narrow; the same widget dragged
 * into a wide column is short and tall-less, and the queue has to stay reachable
 * in both. In an iframe the viewport *is* the panel, so plain media queries are
 * already "the size we have been given" and no script has to measure anything.
 *
 * The narrow case is the document: one column, and the page scrolls. The wide
 * case is two fixed columns that scroll independently, so a long queue never
 * scrolls the transport off the screen.
 */

.layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--gap);
  min-height: 0;
}

.layout__col {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  min-width: 0;
  min-height: 0;
}

#player { display: flex; flex-direction: column; gap: var(--gap); }

/* The transport row. Wraps rather than shrinking: a 24px hit target on a phone
   is not a hit target. */
.player__controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
}

.btn--transport { padding: 6px 14px; font-size: 15px; }

.player__spacer { flex: 1; }

.volume { display: flex; align-items: center; gap: 2px; }

/* Fixed so the readout holds its width while the number changes, like the
   queue lengths elsewhere on the panel. */
.volume__readout {
  min-width: 4.5em;
  text-align: center;
  font-size: 12px;
  color: var(--fg-muted);
}

.player__sub {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  font-size: 13px;
}

/* --- switches ----------------------------------------------------------- */

.switches { display: flex; flex-wrap: wrap; gap: 4px; }

/* A switch is a size down from the transport and flat until it is on: five
   outlined buttons of equal weight read as five primary actions. */
.switch {
  padding: 3px 10px;
  background: transparent;
  color: var(--fg-muted);
}

/* An on switch takes the accent *background*, not only its colour: the base
   rule is transparent, so setting the text to the background colour without it
   renders the label invisible — a row of empty boxes with no hint why. */
.switch.btn--active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
}

/* --- search ------------------------------------------------------------- */

.search { display: flex; flex-direction: column; gap: 6px; }

.search__row { display: flex; gap: 6px; align-items: stretch; }

.search__input, .search__kind {
  font: inherit;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 6px 10px;
  min-width: 0;
}

.search__input { flex: 1; }
.search__kind { flex: none; max-width: 7em; }
/* Chromium's own search affordance is a different size from the field. */
.search__input::-webkit-search-cancel-button { appearance: none; }

.results {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg);
  overflow: hidden;
}

.results__item + .results__item { border-top: 1px solid var(--border); }

/* A row is a form, so the whole thing is one submit target and the fields
   travel as fields. */
.results__pick {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
}

.results__pick:hover { background: var(--bg-subtle); }

.results__label { flex: 1; min-width: 0; display: flex; flex-direction: column; }

.results__title {
  font-size: 13px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.results__by { font-size: 11px; }

/* --- messages ----------------------------------------------------------- */

/* One region, outside every swap target: a refusal has to outlive the redraw it
   caused, and the next successful press clears it out of band. */
.note {
  margin: 0;
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--bg);
  font-size: 12px;
}

.note--error { border-color: var(--danger); color: var(--danger); }
.note--ok { border-color: var(--accent); }

/* No cap here, deliberately. A ceiling on the list in the narrow layout means
   two scrollers nested — the document, and a queue inside it — which is the
   thing people lose their place in. The document is the scroller, and scrolling
   up reaches the transport. The two-column layout below caps the list instead,
   because there the document does not scroll at all. */

/* --- the two-column layout --------------------------------------------- */

@media (min-width: 460px) {
  /* Two columns need a definite height for the columns to scroll inside: the
     point of the shape is that the queue scrolls while the transport does not
     move. This is the only place the panel is pinned to its frame.
     
     The panel must be allowed to *shrink* here, which is the opposite of the
     narrow case above: a panel that refuses to shrink below its content is
     clipped by the hidden overflow instead of handing the shortfall to the
     columns, and the rows below the fold become unreachable rather than
     scrollable. */
  body.widget .root { height: 100%; overflow: hidden; }
  body.widget .root > .panel { flex: 1 1 auto; min-height: 0; }

  .layout {
    flex: 1;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr);
  }

  /* The left column scrolls as a whole — transport, switches and search belong
     together — while the queue fills its own column and scrolls inside the
     card, so its header stays put. */
  .layout__col--play { overflow-y: auto; }

  /* The queue fills its column and scrolls inside the card, so its header stays
     put while the list moves. This is the only place the list scrolls. */
  .layout__col--queue, #queue, .queue { min-height: 0; }
  #queue, .queue { flex: 1; display: flex; flex-direction: column; }
  .queue__list { flex: 1; min-height: 0; overflow-y: auto; }
}

/* Wide and short — the panel covering a chat column rather than sitting in a
   sidebar. Height is the scarce axis, so the vertical rhythm gives first and
   nothing is dropped: every control stays where it was. */
@media (min-width: 460px) and (max-height: 430px) {
  :root { --gap: 8px; --radius: 6px; }

  .player__title { font-size: 15px; }
  .player__sub, .player__meta { font-size: 11px; }
  .btn--transport { padding: 5px 12px; }
  .queue__item { padding: 2px 2px; font-size: 12px; }
}

/* Very tall and narrow — a phone in a sidebar. The queue is what people came
   for, so it gets the room the search box does not need. */
@media (max-width: 340px) {
  .player__spacer { display: none; }
  .volume__readout { min-width: 3.5em; }
}

/* --- minecraft status ---------------------------------------------------- */

.mc { display: flex; flex-direction: column; gap: var(--gap); }
.mc__head { display: flex; align-items: center; gap: 8px; }
.mc__name { font-weight: 600; }
.mc__motd { overflow-wrap: anywhere; }
.mc__players { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.mc__player { display: flex; align-items: center; gap: 6px; }
.mc__initial {
  flex: none;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--bg-subtle);
  color: var(--fg-muted);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
}
.mc__why, .mc__seen, .mc__latency, .mc__more { margin: 0; font-size: 12px; }

/* The admin console, and the room's tail of what it ran. Present only for a
   viewer who holds both D40 halves; absent (never disabled) for everybody
   else, so nothing here needs a disabled state. */
.mc__admin {
  border-top: 1px solid var(--border);
  margin-top: var(--gap);
  padding-top: var(--gap);
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.mc__consoletitle {
  margin: 0;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.mc__consolemeta { font-weight: 400; text-transform: none; letter-spacing: 0; }
.mc__cmdrow { display: flex; gap: 6px; align-items: stretch; }
.mc__cmdrow .btn { flex: none; }
.mc__cmdinput {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 6px 10px;
}
.mc__consoleerr, .mc__consolewarn, .mc__consolenote { margin: 0; }
.mc__output, .mc__tailout {
  margin: 0;
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-subtle);
  overflow-x: auto;
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}
.mc__tailhead, .mc__tailempty { margin: 0; font-size: 12px; }
.mc__taillist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.mc__tailrow { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; font-size: 12px; }
.mc__tailcmd { font-family: var(--mono); overflow-wrap: anywhere; }
.mc__tailrow .mc__tailout { flex-basis: 100%; }

/* Dota dashboard (docs/13). Read-only: a roster of linked players over the
   recent-matches log, with the stack verdict called out. */
.dota { display: flex; flex-direction: column; gap: var(--gap); }
.dota__why, .dota__nomatch { margin: 0; }
.dota__roster { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.dota__player { display: flex; align-items: center; gap: 10px; }
.dota__avatar {
  width: 32px; height: 32px; flex: none; object-fit: cover;
  border-radius: var(--radius-sm); background: var(--border);
}
.dota__who { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.dota__persona { font-weight: 600; }
.dota__meta { font-size: 12px; }
.dota__form { display: inline-flex; gap: 2px; }
.dota__res {
  width: 16px; height: 16px; line-height: 16px; text-align: center;
  border-radius: 3px; font-size: 10px; font-weight: 700;
}
.dota__res--W { background: var(--accent); color: var(--bg); }
.dota__res--L { background: var(--border); color: var(--fg-muted); }
.dota__matches { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.dota__match { border-top: 1px solid var(--border); padding-top: 8px; }
.dota__matchhead { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; }
.dota__result { font-weight: 600; }
.dota__result--radiant { color: var(--accent); }
.dota__result--dire { color: var(--danger); }
.dota__matchmeta { font-size: 12px; }
.dota__stack { margin: 6px 0 0; font-weight: 600; }
.dota__dunno { margin: 6px 0 0; font-size: 12px; }
.dota__parts {
  list-style: none; margin: 6px 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 2px; font-size: 13px;
}
.dota__part { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
.dota__partwho { min-width: 72px; }
.dota__hero { color: var(--fg-muted); }
.dota__kda { font-family: var(--mono); }
.dota__partres { font-size: 12px; }
.dota__partres.dota__w { color: var(--accent); }
.dota__partres.dota__l { color: var(--fg-muted); }
