/* The values come from the portfolio: src/index.css and src/styles/portfolio.css in kevintsyen-portfolio.
   Copy a palette change there to here. app.ts gives the map tokens to the canvas, so each value is a full color,
   not a bare "152 76% 52%" as in the portfolio. */
:root {
  color-scheme: dark;
  --accent: hsl(152 76% 52%);
  --accent-text: hsl(160 20% 4%);
  --bg: hsl(160 18% 4%);
  --panel: hsl(160 16% 5.5%);
  --ocean: hsl(160 14% 7%);
  --land: hsl(160 8% 14%);
  --border: hsl(160 10% 20%);
  --rim: hsl(160 10% 20%);
  --rule: hsl(160 10% 16%);
  --graticule: hsl(152 76% 52% / 0.06);
  --selected-land: hsl(152 76% 52% / 0.22);
  --selected-border: var(--accent);
  --dot: var(--accent);
  --dot-glow: hsl(152 76% 52% / 0.16);
  --live: var(--accent);
  /* The yellow and red dots of the profile.ts window on the portfolio. */
  --load: hsl(45 90% 55%);
  --fail: hsl(0 70% 60%);
  --text: hsl(150 20% 96%);
  --muted: hsl(155 8% 62%);
  --row-hover: hsl(160 14% 7%);
  --row-active: hsl(152 76% 52% / 0.1);
  --selection: hsl(152 76% 52% / 0.3);
  --scroll: hsl(152 76% 52% / 0.5);
  --sans: "Inter", system-ui, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, monospace;
  --dur-micro: 120ms;
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

* { box-sizing: border-box; scrollbar-width: thin; scrollbar-color: var(--scroll) var(--bg); }

body {
  margin: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--bg);
  color: var(--text);
  font: 14px/1.4 var(--sans);
  -webkit-font-smoothing: antialiased;
}

button, input { font: inherit; color: inherit; }
p { margin: 0; }
::selection { background: var(--selection); color: var(--text); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.num { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.label {
  margin: 0;
  font: 500 12px/1 var(--mono);
  color: var(--muted);
}

/* The nav bar of the portfolio: "~/kevin", then a prompt line. The same height (61 px) and font,
   so the bar does not jump when a visitor comes from the portfolio. */
#nav {
  flex: none;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 1ch;
  row-gap: 4px;
  padding: 20px 26px;
  border-bottom: 1px solid var(--rule);
  font: 0.9rem/1.4 var(--mono);
}
#nav a { color: var(--text); text-decoration: none; transition: color var(--dur-micro) var(--ease); }
#nav a:hover, #nav a:focus-visible { color: var(--accent); }
.prompt { display: flex; align-items: baseline; margin-right: auto; }
.sigil { margin-right: 1ch; color: var(--accent); }
h1 { margin: 0; font: inherit; color: var(--text); }
.caret {
  display: inline-block;
  width: 0.6ch;
  height: 1.05em;
  margin-left: 2px;
  align-self: center;
  background: var(--accent);
  animation: blink 1.05s steps(1) infinite;
}
@keyframes blink { 0%, 55% { opacity: 1; } 56%, 100% { opacity: 0; } }
#plate-count { font-size: 12px; color: var(--muted); }
#plate-count .num { color: var(--text); }

main { flex: 1; display: flex; min-height: 0; }

#globe {
  flex: 1;
  min-width: 0;
  position: relative;
  overflow: hidden;
}

#map {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  touch-action: none;
}

/* The playing station. app.ts moves it on each draw. Square, as the dots. */
#ping {
  position: absolute;
  top: 0;
  left: 0;
  width: 22px;
  height: 22px;
  margin: -11px 0 0 -11px;
  border: 1.5px solid currentColor;
  color: var(--muted);
  pointer-events: none;
}
#ping[hidden] { display: none; }
#ping::after {
  content: "";
  position: absolute;
  inset: -1.5px;
  border: 1.5px solid currentColor;
  opacity: 0;
}
[data-tune="live"] #ping { color: var(--live); }
[data-tune="live"] #ping::after { animation: ping 1.8s ease-out infinite; }
[data-tune="load"] #ping { color: var(--load); }
[data-tune="fail"] #ping { color: var(--fail); }
@keyframes ping {
  from { transform: scale(1); opacity: 0.9; }
  to { transform: scale(2.8); opacity: 0; }
}

/* The loader, as terminal output: a caret, a bar, and the step. app.ts hides it when the page is ready. */
#loading {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 0 16px;
  text-align: center;
  font: 13px/1.6 var(--mono);
  pointer-events: none;
}
#loading[hidden], #plate-count[hidden] { display: none; }
#loading .label { display: flex; align-items: center; color: var(--text); }
#load-bar {
  width: min(260px, 80%);
  height: 2px;
  -webkit-appearance: none;
  appearance: none;
  border: 0;
  background: var(--rule);
  overflow: hidden;
}
#load-bar[hidden] { display: none; }
#load-bar::-webkit-progress-bar { background: var(--rule); }
#load-bar::-webkit-progress-value { background: var(--accent); transition: width 0.2s; }
#load-bar::-moz-progress-bar { background: var(--accent); }
/* With no value, a short segment sweeps the track. */
#load-bar:indeterminate {
  background: linear-gradient(90deg, transparent, var(--accent), transparent) 0 0 / 40% 100% no-repeat var(--rule);
  animation: sweep 1.2s ease-in-out infinite alternate;
}
#load-bar:indeterminate::-webkit-progress-bar { background: transparent; }
#load-bar:indeterminate::-moz-progress-bar { background: transparent; }
@keyframes sweep { from { background-position: -100% 0; } to { background-position: 200% 0; } }
#load-text { color: var(--muted); }
#load-bytes { min-height: 1.6em; font-size: 12px; color: var(--muted); }
#loading.failed .caret { background: var(--fail); animation: none; }
#loading.failed #load-text { color: var(--fail); }

aside {
  flex: none;
  width: 400px;
  display: flex;
  flex-direction: column;
  min-height: 0;
  padding: 18px 0 0;
  background: var(--panel);
  border-left: 1px solid var(--rule);
}

/* The icon stroke is the hex of --muted. A data URI cannot read a token. */
#search {
  flex: none;
  margin: 0 18px 20px;
  height: 38px;
  padding: 0 12px 0 36px;
  background: var(--bg) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%2396a69f' stroke-width='1.6'%3E%3Ccircle cx='7' cy='7' r='4.5'/%3E%3Cpath d='M10.5 10.5 14 14'/%3E%3C/svg%3E") 12px 50% no-repeat;
  border: 1px solid var(--border);
  border-radius: 0;
  transition: border-color var(--dur-micro) var(--ease);
}
#search::placeholder { color: var(--muted); }
#search:focus { outline: none; border-color: var(--accent); }
#search:disabled { opacity: 0.5; }

aside > .label {
  flex: none;
  padding: 0 18px 10px;
  border-bottom: 1px solid var(--rule);
}

#title { flex: none; }
#title[hidden] { display: none; }
#title p {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  padding: 14px 18px 12px;
  color: var(--muted);
  font: 12px/1.4 var(--mono);
}
#title-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font: 600 16px/1.1 var(--mono);
  letter-spacing: -0.02em;
  color: var(--text);
}
#title-count { color: var(--text); }
/* Keep the count on one line. The name gets shorter instead. */
#title p > :last-child { flex: none; white-space: nowrap; }

.cols, .row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 0 18px;
}
.cols {
  height: 26px;
  border-bottom: 1px solid var(--rule);
  font: 12px/1 var(--mono);
  color: var(--muted);
}
.cols span:first-child, .row .no { flex: none; width: 30px; }
.cols span:nth-child(2), .row .name { flex: 1; min-width: 0; }

#list { flex: 1; overflow-y: auto; }

.empty { padding: 18px; color: var(--muted); font-size: 13px; }

/* The log before the first click: short numbered steps. */
.help {
  margin: 0;
  padding: 6px 18px;
  list-style: none;
  counter-reset: step;
  color: var(--muted);
}
.help li {
  display: flex;
  gap: 14px;
  padding: 12px 0;
  border-bottom: 1px solid var(--rule);
  counter-increment: step;
}
.help li::before {
  content: "00" counter(step);
  flex: none;
  width: 30px;
  font: 12px/1.6 var(--mono);
}

.row {
  width: 100%;
  height: 40px;
  text-align: left;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--rule);
  cursor: pointer;
  transition: background-color var(--dur-micro) var(--ease);
}
.row:hover { background: var(--row-hover); }
/* Inside the scroll box, an outline outside the row is cut off. */
.row:focus-visible { outline-offset: -2px; }
.row .no { font: 12px var(--mono); color: var(--muted); }
.row .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cols .cc, .row .cc { width: 22px; flex: none; }
.row .cc, .row .meta { flex: none; font: 12px var(--mono); color: var(--muted); }
.row[aria-current="true"] { background: var(--row-active); }
.row[aria-current="true"] .name { color: var(--accent); font-weight: 500; }
.row[aria-current="true"] .no { color: var(--accent); }

/* The tuner: a dial with a needle, then the buttons, the station and the volume. */
#bar {
  flex: none;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas: "dial dial dial" "knobs info vol";
  align-items: center;
  column-gap: 20px;
  row-gap: 12px;
  padding: 14px 26px 14px;
  background: var(--panel);
  border-top: 1px solid var(--rule);
}

#dial {
  grid-area: dial;
  height: 38px;
  padding: 0 22px;
  background: var(--bg);
  border: 1px solid var(--rule);
}
/* Ticks every 5 degrees, long ticks every 30 degrees. The extra 1px shows the last tick. */
#scale {
  position: relative;
  height: 100%;
  margin-right: -1px;
  background:
    repeating-linear-gradient(to right, var(--muted) 0 1px, transparent 1px 8.3333%) 0 0 / 100% 12px no-repeat,
    repeating-linear-gradient(to right, var(--border) 0 1px, transparent 1px 1.38889%) 0 0 / 100% 6px no-repeat;
}
#scale span {
  position: absolute;
  bottom: 5px;
  transform: translateX(-50%);
  font: 11px/1 var(--mono);
  color: var(--muted);
}
/* A label every 60 degrees, on the same scale as dialPercent() in lib.ts. */
#scale span:nth-of-type(1) { left: 0; }
#scale span:nth-of-type(2) { left: 16.667%; }
#scale span:nth-of-type(3) { left: 33.333%; }
#scale span:nth-of-type(4) { left: 50%; }
#scale span:nth-of-type(5) { left: 66.667%; }
#scale span:nth-of-type(6) { left: 83.333%; }
#scale span:nth-of-type(7) { left: 100%; }
#needle {
  position: absolute;
  top: -1px;
  bottom: -1px;
  left: 50%;
  width: 2px;
  margin-left: -1px;
  background: var(--accent);
  transition: left 0.9s cubic-bezier(0.3, 0.7, 0.2, 1);
}

/* Square buttons, as .btn and .btn-primary on the portfolio. */
.knobs { grid-area: knobs; display: flex; align-items: center; gap: 10px; }
.knob {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--muted);
  cursor: pointer;
  transition: color var(--dur-micro) var(--ease), border-color var(--dur-micro) var(--ease);
}
.knob:hover:not(:disabled) { color: var(--accent); border-color: var(--accent); }
#toggle { color: var(--accent-text); background: var(--accent); border-color: var(--accent); }
.knob.small { width: 36px; height: 36px; }
.knob:disabled, #toggle:disabled { color: var(--muted); background: none; border-color: var(--border); cursor: default; }
#toggle .i-pause, #toggle.on .i-play { display: none; }
#toggle.on .i-pause { display: block; }

.info { grid-area: info; min-width: 0; }
.line { display: flex; align-items: center; gap: 10px; min-width: 0; }
/* An empty line keeps its height, and hidden details keep their space. So the bar has one height with and
   without a station. A new bar height changes the globe size, and the globe jumps under the pointer. */
.line.sub { min-height: 1.4em; margin-top: 3px; padding-left: 20px; font-size: 13px; color: var(--muted); }
#now { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 17px; font-weight: 500; }
#tuned { flex: none; }
/* app.ts adds .on to #bar while a station is in the player. */
#bar:not(.on) #tuned { display: none; }
#bar:not(.on) #now { color: var(--muted); }
/* With no station, hide the needle. At the middle it would cover the "0" label. */
#bar:not(.on) #needle { opacity: 0; }

/* The status light. app.ts sets data-tune on <body>. */
#led {
  flex: none;
  width: 10px;
  height: 10px;
  background: var(--border);
}
[data-tune="live"] #led { background: var(--live); }
[data-tune="load"] #led { background: var(--load); animation: blink 1.05s steps(1) infinite; }
[data-tune="fail"] #led { background: var(--fail); }
[data-tune="live"] #status { color: var(--live); }
[data-tune="load"] #status { color: var(--load); }
[data-tune="fail"] #status { color: var(--fail); }

/* Keep "Stream failed" on one line. A second line makes the bar higher after the playing row scrolled into view,
   and on a phone the bar then covers the row. */
#status { flex: none; white-space: nowrap; }
#place { min-width: 0; max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#place:empty, #language:empty, #tags:empty { display: none; }
#details[hidden] { visibility: hidden; }
#homepage[hidden] { display: none; }
#details > * { flex: none; }
#format { font-size: 12px; }
#language { text-transform: capitalize; }
#tags { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#homepage { color: var(--muted); text-decoration: none; }
#where { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: var(--muted); text-decoration: none; }
#where, #homepage { transition: color var(--dur-micro) var(--ease); }
#where::after, #homepage::after { content: " ↗"; }
#where:hover, #where:focus-visible, #homepage:hover, #homepage:focus-visible { color: var(--accent); }

.vol { grid-area: vol; display: flex; align-items: center; gap: 10px; }
/* Chrome draws the native track near-white, also with accent-color. So draw the track here.
   app.ts sets --v, the filled part. */
#volume {
  width: 140px;
  min-width: 0;
  margin: 0;
  -webkit-appearance: none;
  appearance: none;
  height: 2px;
  background: linear-gradient(var(--accent) 0 0) 0 / var(--v, 70%) 100% no-repeat, var(--border);
}
#volume::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; background: var(--text); }
#volume::-moz-range-thumb { width: 12px; height: 12px; border: 0; border-radius: 0; background: var(--text); }
#volume:focus-visible { outline-offset: 6px; }
#volume-text { width: 4ch; text-align: right; font-size: 12px; color: var(--muted); }

/* A touch screen has no mouse wheel, and a mouse cannot pinch. Each one gets its own zoom line.
   The shown line keeps the flex of .help li, or the step number loses its 14 px gap. */
.help .pinch { display: none; }
@media (hover: none) {
  .help .wheel { display: none; }
  .help .pinch { display: flex; }
}

@media (prefers-reduced-motion: reduce) {
  #ping::after, #led, .caret, #load-bar { animation: none !important; }
  #needle { transition: none; }
}

@media (max-width: 800px) {
  body { height: auto; overflow: visible; }
  #nav { padding: 20px 16px; }
  main { flex-direction: column; }
  #globe { flex: none; height: 55vh; }
  aside { width: auto; padding-top: 16px; border-left: 0; border-top: 1px solid var(--rule); }
  #search { margin: 0 16px 16px; }
  aside > .label, #title p, .cols, .row { padding-left: 16px; padding-right: 16px; }
  .row { height: 44px; }
  #list { max-height: 60vh; }
  /* Keep the tuner in view when the log is long. */
  #bar {
    position: sticky;
    bottom: 0;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas: "dial dial" "knobs info";
    column-gap: 12px;
    row-gap: 10px;
    padding: 10px 16px 12px;
  }
  #dial { height: 30px; padding: 0 16px; }
  #scale span { bottom: 3px; }
  .knobs { gap: 8px; }
  .knob { width: 40px; height: 40px; }
  .knob.small { width: 34px; height: 34px; }
  #now { font-size: 15px; }
  /* The status light shows the tuned state. Give the space to the station name. */
  #bar.on #tuned { display: none; }
  /* No volume control on a phone (see IDR 0005). iOS ignores player.volume, so the slider did nothing.
     The device buttons set the volume. The bar drops the vol column above, and the tuner keeps one row. */
  .vol { display: none; }
  .line.sub { padding-left: 0; gap: 8px; }
  /* The details line has the whole bar width now. The format and the homepage link keep their width.
     The tags get the space that is left, and the language gets shorter when it does not fit.
     So the line never makes the page wider. */
  #language { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  #tags { flex-basis: 0; }
  /* Keep the row that scrollIntoView shows above the sticky #bar. */
  html { scroll-padding-bottom: 145px; }
}
