/* PIXELBOY Web.
   Flat, hairline-ruled, monospaced: a piece of equipment, not a dashboard.
   No cards, no radii, no shadows, one accent used only for "live". */

/* Press Start 2P, SIL Open Font License 1.1 - see THIRD_PARTY_NOTICES.md.
   Self-hosted rather than linked from a font CDN: the app works offline, and
   a stylesheet from someone else's server is a dependency the service worker
   cannot answer for. Only the latin subset, 12KB. */
@font-face {
  font-family: 'Press Start 2P';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/press-start-2p.woff2) format('woff2');
}

:root {
  --bg: #0e0e0e;
  --fg: #c9c9c9;
  --dim: #6e6e6e;
  --line: #232323;
  --line-2: #333;
  --live: #c8532f;
  /* The pixel font, with a monospace behind it for the handful of glyphs it
     has no character for. It is an 8x8 font: every size below is a whole
     multiple of 8, which is the only way it renders as drawn. */
  /* The 8x8 pixel circle, as a mask: the slider thumb and the record mark are
     the same shape, and a mask means it is the surrounding colour rather than
     a second copy of it. */
  --pixel-dot: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath d='M2 0h4v1H2zM1 1h6v1H1zM0 2h8v4H0zM1 6h6v1H1zM2 7h4v1H2z'/%3E%3C/svg%3E");
  --font: 'Press Start 2P', ui-monospace, "Cascadia Mono", Consolas, monospace;
  --row: 22px;
  --bar: 38px;
}

* { box-sizing: border-box; }

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

body {
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--fg);
  font: 8px/1.5 var(--font);
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
  -webkit-text-size-adjust: 100%;
  touch-action: manipulation;
}

button, select, input { font: inherit; color: inherit; border-radius: 0; }

/* --------------------------------------------------------------- top row */

/* Wraps rather than clipping: on a phone the nav needs rows of its own, and
   a fixed height would cut them off rather than make room. */
.topbar {
  position: relative;   /* the collapsed nav hangs off this */
  display: flex;
  align-items: stretch;
  flex: none;
  flex-wrap: wrap;
  min-height: calc(var(--bar) + env(safe-area-inset-top));
  padding-top: env(safe-area-inset-top);
  /* Opaque, because the bar is sticky on a phone and the page scrolls under
     it - without this, the picture shows through the controls. */
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}
.topbar > * { min-height: var(--bar); }

.topbar h1 {
  display: flex;
  align-items: center;
  margin: 0;
  padding: 0 14px;
  border-right: 1px solid var(--line);
}

/* The wordmark, at the art's own size and no other: it is 120x22 pixel art and
   the browser is told to keep it that way, so no display density resamples it
   into something soft. */
.topbar h1 img {
  display: block;
  width: 120px;
  height: 22px;
  image-rendering: pixelated;
}

/* On a desktop the nav's groups are laid straight into the bar, as if the
   wrapper were not there. */
.nav { display: contents; }

/* One group of choices, separated from the next by a hairline. The active one
   is filled rather than tinted - at eight pixels a colour shift is a
   suggestion, and this is a choice. */
.segmented {
  display: flex;
  align-items: stretch;
  border-right: 1px solid var(--line);
}

.segmented button,
.actions button {
  background: transparent;
  border: 0;
  padding: 0 13px;
  cursor: pointer;
  color: var(--dim);
}
.segmented button:hover,
.actions button:hover:not(:disabled) { color: var(--fg); background: #151515; }
.segmented button.on { background: var(--fg); color: var(--bg); }

.actions {
  display: flex;
  align-items: stretch;
  margin-left: auto;
  border-left: 1px solid var(--line);
}

button:disabled { opacity: 0.3; cursor: default; }

/* The hamburger, on a phone only. */
.menu-toggle {
  display: none;
  align-items: center;
  border: 0;
  border-left: 1px solid var(--line);
  padding: 0 14px;
}

/* The hamburger, at the art's own size and no other. It is 32x32 pixel art,
   and the browser is told to keep it that way so no display density resamples
   it into something soft. */
.menu-toggle img {
  display: block;
  width: 32px;
  height: 32px;
  image-rendering: pixelated;
}

/* A new build is waiting. Deliberately the one thing in the bar that is not
   grey: it is the only control that is about to stop being true. */
.update-app {
  display: block;
  margin-left: auto;
  border: 0;
  border-left: 1px solid var(--line);
  padding: 0 14px;
  background: var(--live);
  color: var(--bg);
  font-weight: 600;
  cursor: pointer;
}
.update-app[hidden] { display: none; }
.update-app:hover { background: #d9633c; color: var(--bg); }

.menu-backdrop { display: none; }

/* ------------------------------------------------------------------ main */

main {
  flex: 1;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 300px;
  min-height: 0;
}

.viewer {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}

/* Takes the leftover height; view.js sizes the canvas to the largest whole
   multiple of 128x112 that fits this box. */
.stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 120px;
  display: grid;
  place-items: center;
  padding: 14px;
}

/* Sized in pixels by view.js, to a whole multiple of 128x112. Deliberately
   no max-width/max-height: a clamp on one axis alone squeezes that axis and
   stretches the picture, which is worse than the overflow it guards against.
   view.js measures the stage's content box exactly, so it never overflows. */
#view {
  display: block;
  image-rendering: pixelated;
  background: #000;
}

.stage-message {
  position: absolute;
  inset: 0;
  display: none;
  place-items: center;
  padding: 30px;
  text-align: center;
  color: var(--dim);
  background: rgba(14, 14, 14, 0.94);
}
.stage-message.show { display: grid; }
.stage-message strong { display: block; color: var(--fg); font-weight: 600; margin-bottom: 4px; }

/* Transient messages - a failed import, the HDR bracket counting up. Nothing
   is on screen when there is nothing to say, which is most of the time. */
.toast {
  position: absolute;
  left: 10px;
  bottom: 10px;
  max-width: calc(100% - 20px);
  padding: 5px 9px;
  background: rgba(8, 8, 8, 0.86);
  border: 1px solid var(--line-2);
  color: var(--fg);
  font-size: 8px;
  pointer-events: none;
}
.toast[hidden] { display: none; }

.viewer-actions {
  flex: none;
  display: flex;
  align-items: center;
  gap: 0;
  border-top: 1px solid var(--line);
  padding-bottom: env(safe-area-inset-bottom);
}
.viewer-actions .spacer { flex: 1; }

button {
  background: transparent;
  border: 1px solid var(--line-2);
  padding: 5px 12px;
  cursor: pointer;
  font-size: 8px;
  text-transform: uppercase;
  color: var(--fg);
}
button:hover:not(:disabled) { background: #171717; }
button.primary { background: var(--fg); border-color: var(--fg); color: var(--bg); font-weight: 600; }
button.primary:hover:not(:disabled) { background: #e4e4e4; color: var(--bg); }

/* The shutter: the one control with no substitute, so it gets the whole row
   and more height than anything else. */
.viewer-actions > button {
  border: 0;
  align-self: stretch;
  min-height: 44px;
  padding: 0 16px;
  font-size: 8px;
}
/* The shutter takes the room; record is a fixed column beside it. */
#shot { flex: 1; }

#record {
  flex: none;
  min-width: 72px;
  border-left: 1px solid var(--line);
  color: var(--live);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

/* 16px is the 8x8 art at 2x: a whole number, so every step is the same size
   rather than some rows landing on a pixel and others straddling two. */
.rec-mark {
  flex: none;
  width: 16px;
  height: 16px;
  fill: currentColor;
  shape-rendering: crispEdges;
}
.rec-mark .rec-stop { display: none; }
#record.on .rec-mark .rec-dot { display: none; }
#record.on .rec-mark .rec-stop { display: block; }
/* Recording is a state, not a press, so it is shown as one: filled, and with
   the elapsed time where the word was. */
#record.on {
  background: var(--live);
  border-color: var(--live);
  color: var(--bg);
  font-weight: 600;
}

/* ----------------------------------------------------------------- picker */

/* The button that opens one: the current choice, marked, with a caret. */
.picker-open {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 4px 6px;
  border-color: var(--line-2);
  text-align: left;
}
.picker-open .chips {
  flex: none;
  width: 34px;
  height: 16px;
  display: block;
  border: 1px solid var(--line-2);
}
.picker-open canvas.chips { image-rendering: pixelated; height: 22px; width: auto; }
.picker-open .picker-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.picker-open .caret { flex: none; display: flex; color: var(--dim); }

/* 8x8 art at 1x, which is what the row's text is too. */
.picker-open .caret svg {
  width: 8px;
  height: 8px;
  fill: currentColor;
  shape-rendering: crispEdges;
}

.picker {
  padding: 0;
  border: 1px solid var(--line-2);
  background: var(--bg);
  color: var(--fg);
  font: inherit;
  max-width: min(96vw, 760px);
  max-height: 86dvh;
}
.picker::backdrop { background: rgba(0, 0, 0, 0.6); }

.picker-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-bottom: 1px solid var(--line);
  font-size: 8px;
  text-transform: uppercase;
  color: var(--dim);
}
.picker-close {
  margin-left: auto;
  border: 0;
  padding: 0 4px;
  font-size: 16px;
  line-height: 1;
  color: var(--dim);
}
.picker-close:hover { color: var(--fg); background: transparent; }

/* The notices are prose and tables rather than a grid, and they are long, so
   this is the one place in the app that scrolls. */
.about { max-width: min(94vw, 640px); }
.about-body {
  padding: 12px;
  max-height: calc(80dvh - 42px);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.about-body > :first-child { margin-top: 0; }
.about-body h2 {
  margin: 18px 0 8px;
  font-size: 8px;
  color: var(--fg);
  text-transform: uppercase;
}
.about-body h3 { margin: 14px 0 6px; font-size: 8px; color: var(--dim); }
.about-body p { margin: 0 0 10px; color: var(--dim); }
.about-body ul { margin: 0 0 10px; padding-left: 16px; color: var(--dim); }
.about-body li { margin-bottom: 6px; }
.about-body code { color: var(--fg); }
.about-body pre {
  margin: 0 0 12px;
  padding: 8px;
  overflow-x: auto;
  border: 1px solid var(--line);
  color: var(--dim);
}
.about-body table {
  width: 100%;
  margin: 0 0 14px;
  border-collapse: collapse;
}
.about-body th, .about-body td {
  padding: 4px 6px 4px 0;
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--line);
  color: var(--dim);
}
.about-body th { color: var(--fg); }
.about-body a { color: var(--fg); text-decoration: underline; text-underline-offset: 2px; }
.about-body td:last-child { overflow-wrap: anywhere; }

.picker .swatches,
.picker .frame-list { padding: 12px; max-height: calc(86dvh - 44px); overflow-y: auto; }
/* Big enough to tell two palettes apart at a glance, and to hit with a thumb
   without looking. They were 38x30, which is a colour chip rather than a
   thing you can aim at. */
.picker .swatches { grid-template-columns: repeat(auto-fill, minmax(58px, 1fr)); gap: 4px; }
.picker .swatch { height: 44px; }

/* With a picture in them, the swatches are the picture's own size and the
   entry is that with the palette's colours under it. The cell is sized to the
   canvas rather than the canvas to the cell: a 128x112 image squeezed into a
   74px column and then scaled back up by the browser is exactly as soft as it
   sounds, and the dither is the thing being chosen between. */
.picker .swatches.previews { grid-template-columns: repeat(auto-fill, minmax(136px, 1fr)); gap: 8px; }
.picker .swatches.previews .swatch {
  display: flex;
  flex-direction: column;
  height: auto;
  padding: 0;
}
/* Drawn at the canvas's own size and never scaled up - the column is wider
   than the largest of them, so the intrinsic size is what is shown. A pixel
   font and a four-shade dither both stop being themselves the moment a
   browser resamples them. */
.picker .swatches.previews .swatch .preview {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  image-rendering: pixelated;
}
.picker .swatches.previews .swatch .chips { height: 8px; flex: none; }
/* The same, at the frames' own width: a 160x144 canvas in a 104px cell is
   resampled twice. */
.picker .frame-list { grid-template-columns: repeat(auto-fill, minmax(172px, 1fr)); gap: 8px; }
.picker .frame-list canvas { width: 160px; image-rendering: pixelated; }

/* ------------------------------------------------------------------ panel */

/* Dimmed until the page reports itself ready - every control is built from
   the core, so until it has loaded there are none anyway. */
body.booting .panel { opacity: 0.4; }

.panel {
  border-left: 1px solid var(--line);
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

.section { border-bottom: 1px solid var(--line); }

.section > h2 { margin: 0; }
.section > h2 button {
  all: unset;
  /* `all: unset` resets box-sizing to content-box too, which makes the
     width:100% below mean the *content* box - the button then renders 24px
     wider than the panel. Invisible on desktop only because body clips
     overflow there; on a phone it becomes a horizontal scrollbar. */
  box-sizing: border-box;
  display: flex;
  align-items: baseline;
  gap: 8px;
  width: 100%;
  padding: 8px 12px;
  cursor: pointer;
  font-size: 8px;
  text-transform: uppercase;
  color: var(--dim);
}
.section > h2 button:hover { color: var(--fg); }
.section > h2 button::before {
  content: "\2013";
  color: var(--line-2);
  width: 8px;
}
.section.collapsed > h2 button::before { content: "+"; }
.section > h2 .badge {
  margin-left: auto;
  text-transform: none;
  color: var(--fg);
}
.section-body { padding: 2px 12px 12px; }
.section.collapsed .section-body { display: none; }

.row {
  display: grid;
  grid-template-columns: 96px minmax(0, 1fr);
  align-items: center;
  gap: 8px;
  min-height: var(--row);
}
.row > label {
  font-size: 8px;
  text-transform: uppercase;
  color: var(--dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.row > div { display: flex; align-items: center; gap: 7px; min-width: 0; }
.row > div > input[type="range"] { flex: 1; min-width: 0; }
.row .value { font-size: 8px; color: var(--fg); white-space: nowrap; }

input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  height: 14px;
  background: transparent;
}
input[type="range"]::-webkit-slider-runnable-track { height: 2px; background: var(--line-2); }
/* 16px is the 8x8 art at 2x, so every step is the same size. */
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 16px; height: 16px;
  margin-top: -7px;
  background-color: var(--fg);
  border: 0;
  -webkit-mask-image: var(--pixel-dot);
  mask-image: var(--pixel-dot);
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  cursor: ew-resize;
}
input[type="range"]::-moz-range-track { height: 2px; background: var(--line-2); }
input[type="range"]::-moz-range-thumb {
  width: 16px; height: 16px;
  background-color: var(--fg);
  border: 0;
  border-radius: 0;
  mask-image: var(--pixel-dot);
  mask-size: 100% 100%;
  cursor: ew-resize;
}
input[type="range"]:disabled::-webkit-slider-thumb { background-color: var(--line-2); }

input[type="checkbox"] {
  appearance: none;
  width: 13px; height: 13px;
  border: 1px solid var(--line-2);
  background: transparent;
  cursor: pointer;
  margin: 0;
}
input[type="checkbox"]:checked { background: var(--fg); border-color: var(--fg); }

select {
  width: 100%;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--line-2);
  padding: 2px 0;
  font-size: 8px;
  cursor: pointer;
}
select:focus { outline: 0; border-bottom-color: var(--fg); }
option { background: var(--bg); color: var(--fg); }

.buttons { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; }

/* ---------------------------------------------------------------- swatches */

/* No labels: the selected palette's name is in the section header, so the
   grid stays a grid. Names are in the title attribute. */
.swatches {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(26px, 1fr));
  gap: 2px;
}
.swatch {
  border: 1px solid transparent;
  padding: 0;
  background: transparent;
  cursor: pointer;
  display: block;
  height: 18px;
}
.swatch .chips { display: block; width: 100%; height: 100%; }
.swatch:hover { border-color: var(--line-2); }
.swatch.on { border-color: var(--fg); }

.frame-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(52px, 1fr)); gap: 3px; }
.frame-list button { padding: 2px; display: block; border-color: transparent; }
.frame-list canvas { display: block; width: 100%; height: auto; image-rendering: pixelated; }
.frame-list button:hover { border-color: var(--line-2); }
.frame-list button.on { border-color: var(--fg); }

/* ---------------------------------------------------------------- footer */

.panel-foot {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 4px 12px 6px;
  font-size: 8px;
  color: var(--dim);
}
.panel-foot button {
  margin-left: auto;
  border: 0;
  padding: 0;
  font-size: 8px;
  text-transform: uppercase;
  color: var(--dim);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.panel-foot button:hover { color: var(--fg); background: transparent; }

/* ===================================================================
   Touch. Sized up wherever the pointer is coarse rather than in a width
   media query: a touchscreen laptop wants big targets at any width, a
   narrow window with a mouse does not.
   =================================================================== */

@media (pointer: coarse) {
  :root { --row: 40px; --bar: 46px; }
  .segmented button,
  .section > h2 button { padding: 13px 12px; font-size: 8px; }
  .section-body { padding: 4px 12px 14px; }
  .row { grid-template-columns: 104px minmax(0, 1fr); }
  .row > label { font-size: 8px; }
  input[type="range"] { height: 34px; }
  input[type="range"]::-webkit-slider-thumb { width: 24px; height: 24px; margin-top: -11px; }
  input[type="range"]::-moz-range-thumb { width: 24px; height: 24px; }
  input[type="checkbox"] { width: 22px; height: 22px; }
  select { padding: 10px 0; font-size: 8px; }
  .swatches { grid-template-columns: repeat(auto-fill, minmax(38px, 1fr)); gap: 3px; }
  .swatch { height: 30px; }
  .frame-list { grid-template-columns: repeat(auto-fill, minmax(64px, 1fr)); }
  button { padding: 10px 14px; font-size: 8px; }
  .viewer-actions > button { min-height: 58px; }
  #record { min-width: 88px; }
}

/* ===================================================================
   Narrow. Portrait phones take the viewfinder first and the panel below
   it in one scroll; landscape phones keep two columns but narrow,
   because at ~380px tall there is no height to stack in.
   =================================================================== */

/* Portrait phones scroll the document rather than nesting scroll areas. The
   whole fixed-height flex chain is dropped here - it is what clipped the
   panel to one section - and the page becomes an ordinary tall document. */
@media (max-width: 860px) {
  /* The page fits the screen and does not scroll at all: the top bar and the
     panel are sized by their content, the viewfinder takes what is left, and
     nothing is measured in dvh - which is safe here precisely because there
     is no scrolling for it to interfere with. */

  html, body { height: 100%; height: 100dvh; }
  body { display: flex; flex-direction: column; overflow: hidden; }
  main { flex: 1; display: flex; flex-direction: column; min-height: 0; }

  /* The picture is 8:7, so its height follows from its width. Taking the
     height it needs for the width it has, rather than all the height there is,
     is what leaves the rest of the screen to the panel - the viewfinder was
     filling the leftover and letterboxing a picture that had stopped growing
     at the width of the screen, with the difference going to nothing. */
  .viewer {
    display: flex;
    flex-direction: column;
    flex: 0 2 auto;      /* gives way first, so the panel keeps its height */
    min-height: 0;
  }
  .stage {
    flex: 0 0 auto;
    aspect-ratio: 8 / 7;
    min-height: 110px;
    padding: 6px;
  }

  /* Everything the stage does not want. It scrolls only when the controls
     need more room than there is, which is now the exception rather than the
     rule - sections collapse, and the palettes and frames are popups. */
  .panel {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    border-left: 0;
    border-top: 1px solid var(--line);
  }

  /* The bar stays put, so the menu button is always within reach. */
  .topbar { position: sticky; top: 0; z-index: 30; }

  /* The nav collapses behind the menu button. Mode and source are set once
     and then left alone, and as two stacked rows they cost a third of a phone
     screen before the viewfinder even starts. */
  .menu-toggle { display: flex; margin-left: auto; }

  /* Dims the page behind the open nav, and taps it to close. Inert the rest
     of the time - a full-viewport layer that could swallow a touch meant for
     the page would be a far worse bug than an undimmed menu. */
  .menu-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 20;
    background: rgba(0, 0, 0, 0.5);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.14s ease-out;
  }
  body.menu-open .menu-backdrop { opacity: 1; pointer-events: auto; }

  .nav { display: none; }
  body.menu-open .nav {
    display: block;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 20;
    background: var(--bg);
    border-bottom: 1px solid var(--line-2);
    padding-bottom: env(safe-area-inset-bottom);
  }
  /* Each row is a group, at its own width rather than stretched: a GB button
     the width of the screen reads as a heading, not as one of two choices. */
  .nav .segmented,
  .nav .actions {
    min-height: var(--bar);
    border: 0;
    border-top: 1px solid var(--line);
    padding: 0 6px;
  }
  .nav .segmented:first-child { border-top: 0; }
  .nav .actions { margin-left: 0; }
  .nav .segmented button,
  .nav .actions button { padding: 0 16px; }

  /* Tighter than the desktop panel, to leave the picture its room. */
  .section > h2 button { padding: 6px 12px; }
  .section-body { padding: 2px 12px 8px; }
}

/* Landscape puts the fixed-height column layout back: there is no height to
   scroll in, so the panel has to be its own scroll area beside the picture. */
@media (max-width: 860px) and (orientation: landscape) {
  html, body { height: 100%; height: 100dvh; }
  body { display: flex; flex-direction: column; overflow: hidden; }
  main {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 260px;
    flex: 1;
    min-height: 0;
  }
  .viewer { display: flex; flex-direction: column; min-height: 0; }
  .stage { height: auto; flex: 1 1 auto; min-height: 0; }
  .panel {
    border-left: 1px solid var(--line);
    border-top: 0;
    max-height: none;
    min-height: 0;
    overflow-y: auto;
  }
}

/* A short viewport: stop reserving height for things that can scroll instead. */
@media (max-height: 460px) {
  .topbar h1 { display: none; }
  .stage { min-height: 90px; }
}
