/* Manrope, served from this origin: no request to a third party carries the
   visitor's address, and the page's policy can stay `font-src 'self'`.
   SIL Open Font License — `fonts/OFL.txt`. */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('fonts/manrope-latin-ext.woff2') format('woff2');
  unicode-range:
    U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
    U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113,
    U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('fonts/manrope-latin.woff2') format('woff2');
  unicode-range:
    U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308,
    U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ------------------------------------------------------------------
   Feengster's palette: the brand's primary family and IBM Carbon's
   neutral ramp, as literals.
   ------------------------------------------------------------------ */
:root {
  --brand-5: #5654ff;
  --n0: #f4f4f4;
  --n1: #e0e0e0;
  --n2: #c6c6c6;
  --n5: #6f6f6f;
  --n7: #393939;
  --n8: #262626;
  --n9: #161616;
  --white: #ffffff;
  --shade: #000000;
  --danger: #da1e28;
  --pill: 32px;
  /* The bar's height, declared rather than left to the padding: the mark
     is the tallest thing in the row, so an implicit height would be
     whatever the mark happens to be plus padding. */
  --bar: 60px;
  /* The wordmark is drawn 44px tall and both files are 917 tall. Where
     the word sits inside those 917 is not the same in the two of them, so
     that number is declared per scheme beside the file it describes. */
  --logo-h: 44px;
  --logo-file-h: 917;
}

/* A glass material — one translucent plane that keeps its controls
   legible whatever scrolls behind it. Declared per scheme, not with
   `light-dark()`. */
:root[data-theme='light'] {
  color-scheme: light;
  --ground: var(--n0);
  --ink: var(--n9);
  --ink-quiet: var(--n5);
  --rule: color-mix(in srgb, var(--shade) 12%, transparent);
  --glass-fill: color-mix(in srgb, var(--white) 72%, transparent);
  --glass-border: color-mix(in srgb, var(--shade) 8%, transparent);
  --glass-shadow:
    0 8px 28px color-mix(in srgb, var(--shade) 12%, transparent),
    inset 0 1px 0 0 color-mix(in srgb, var(--white) 50%, transparent);
  --glass-control: var(--n7);
  --glass-control-fill: color-mix(in srgb, var(--white) 45%, transparent);
  --glass-control-fill-hover: color-mix(in srgb, var(--white) 65%, transparent);
  --grid-fine: color-mix(in srgb, var(--shade) 5%, transparent);
  --grid-major: color-mix(in srgb, var(--shade) 9%, transparent);
  --chassis: #d8d8d8;
  --chassis-edge: #bdbdbd;
  --screen-fill: var(--white);
  --screen-lift: 0 10px 28px color-mix(in srgb, var(--shade) 16%, transparent);
  --logo-on-dark: none;
  --logo-on-light: block;
  /* `logo-dark.png`: the capital of "Feengster" runs y 326 -> 548 of 917,
     so the word's baseline is y 548. Read off the pixels, not judged. */
  --logo-word-baseline: 548;
  --chrome-tabs: #dfe1e5;
  --chrome-bar: #ffffff;
  --chrome-field: #f1f3f4;
  --chrome-ink: #202124;
  --chrome-quiet: #5f6368;
  --chrome-rule: #dadce0;
  --os-bar: #ececec;
}
:root[data-theme='dark'] {
  color-scheme: dark;
  --ground: var(--n9);
  --ink: var(--n0);
  --ink-quiet: color-mix(in srgb, var(--white) 55%, transparent);
  --rule: color-mix(in srgb, var(--white) 14%, transparent);
  --glass-fill: color-mix(in srgb, var(--n9) 62%, transparent);
  --glass-border: color-mix(in srgb, var(--white) 12%, transparent);
  --glass-shadow:
    0 8px 28px color-mix(in srgb, var(--shade) 45%, transparent),
    inset 0 1px 0 0 color-mix(in srgb, var(--white) 8%, transparent);
  --glass-control: var(--n2);
  --glass-control-fill: color-mix(in srgb, var(--white) 6%, transparent);
  --glass-control-fill-hover: color-mix(in srgb, var(--white) 11%, transparent);
  --grid-fine: color-mix(in srgb, var(--white) 4%, transparent);
  --grid-major: color-mix(in srgb, var(--white) 8%, transparent);
  --chassis: #2b2b30;
  --chassis-edge: #3c3c43;
  --screen-fill: var(--n8);
  --screen-lift: 0 10px 30px color-mix(in srgb, var(--shade) 60%, transparent);
  --logo-on-dark: block;
  --logo-on-light: none;
  /* `logo-light.png`: the same drawing, framed 47px higher in the file —
     the capital runs y 279 -> 501 of 917, so the baseline is y 501. The
     two files are the same artwork with different padding, which is why
     one number for both left the word and the text apart by 2.25px on
     screen. */
  --logo-word-baseline: 501;
  --chrome-tabs: #202124;
  --chrome-bar: #35363a;
  --chrome-field: #202124;
  --chrome-ink: #e8eaed;
  --chrome-quiet: #9aa0a6;
  --chrome-rule: #3c4043;
  --os-bar: #1c1c1e;
}

* {
  box-sizing: border-box;
}
html,
body {
  height: 100%;
  overflow: hidden;
}
body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: 'Manrope', ui-sans-serif, system-ui, sans-serif;
  font-size: 14px;
  line-height: 1.45;
}

/* ==================================================================
   The bar: two groups under `space-between`, neither of which wraps —
   the brand and the chain of context on the left, the actions on the
   right.
   ================================================================== */
header {
  position: fixed;
  top: 12px;
  left: 12px;
  right: 12px;
  z-index: 200;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  height: var(--bar);
  padding: 0 10px 0 16px;
  border: 1px solid var(--glass-border);
  border-radius: var(--pill);
  background: var(--glass-fill);
  box-shadow: var(--glass-shadow);
  backdrop-filter: blur(18px) saturate(180%);
  -webkit-backdrop-filter: blur(18px) saturate(180%);
}
.group {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: nowrap;
}
.group.context {
  flex: 1 1 auto;
  min-width: 0;
}
.group.actions {
  flex: 0 0 auto;
}
/* The chain of context, which is one line of text: the wordmark, the word
   that follows it, and the name of the application. `baseline` and not
   `center`, so the browser puts them on one line instead of three boxes
   of different heights being centred against each other — which is what
   had the app's name sitting 2.9px below "Lab". The height is the drawing's
   own, so the row measures what it did before and the rule between the
   items keeps centring itself. */
.chain {
  display: flex;
  align-items: baseline;
  gap: 6px;
  flex: 0 0 auto;
  height: var(--logo-h);
}
.brand {
  display: flex;
  align-items: baseline;
  gap: 9px;
  flex: 0 0 auto;
  height: var(--logo-h);
  color: inherit;
  text-decoration: none;
}
/* The mark is drawn at a height of 44, its width left to the image.

   An image's baseline, to a flex row, is the bottom edge of its margin
   box. Raising that edge by exactly the drawing left under the word's
   baseline puts the row's baseline on the word itself, and every text
   beside it lands there without anyone writing a pixel: the browser does
   the arithmetic, from the file's own numbers. */
/* The box that carries the baseline, and the whole of the alignment.
   Flexbox synthesizes a baseline for an item that has none from its
   *border box*, and aligns by the distance from the top of its margin box
   to it — so a negative bottom margin does not move it, which is what
   measuring said: the row's baseline stayed at the foot of the drawing,
   y 64.00, while the word's was at 44.04.

   The height does move it. This box is exactly as tall as the word's
   baseline is deep inside the drawing, so its own bottom edge — which is
   where a box with no text lines inside it puts its baseline — lands on
   the word. The drawing overflows below it and nothing measures this box
   but the browser, to align the text beside it. */
.mark {
  display: inline-block;
  flex: 0 0 auto;
  height: calc(var(--logo-h) * var(--logo-word-baseline) / var(--logo-file-h));
}
.brand img {
  display: block;
  height: var(--logo-h);
  width: auto;
}
/* One file per scheme. The token's value is `block`, which is also what
   the rule above sets, so the drawing keeps being the only thing in its
   box and the box keeps being what the row aligns to. */
.brand img.on-dark {
  display: var(--logo-on-dark);
}
.brand img.on-light {
  display: var(--logo-on-light);
}
/* `Lab` is set to the wordmark's own letters, and the number is measured,
   not judged — three guesses were wrong before it was.

   The file is 2626×917 and it holds two things: the symbol, x 23→568, and
   the word "Feengster", x 693→2604. The symbol is what makes the file
   tall, so the word inside it is small: its capital runs y 279→502, which
   is 223px of 917, and at the 44px draw that is **10.7px**
   of capital on screen. A capital in Manrope is about 0.72em, so the word
   beside it is 15px. Anything larger is a `Lab` bigger than the brand it
   belongs to, which is what 24, 27 and 36 all were. */
.brand em {
  font-size: 15px;
  font-style: normal;
  font-weight: 600;
  letter-spacing: 0.01em;
  line-height: 1;
}
/* The application's own name, after the house's. Subordinate to the
   brand. */
.app {
  flex: 0 0 auto;
  color: var(--ink-quiet);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.02em;
  white-space: nowrap;
  line-height: 1;
}
/* The chain's own rule and slash. */
.divider {
  align-self: center;
  flex: 0 0 auto;
  width: 1px;
  height: 24px;
  margin-inline: 6px;
  background: var(--rule);
}
.slash {
  flex: 0 0 auto;
  opacity: 0.4;
  user-select: none;
}

input {
  min-width: 0;
  padding: 7px 13px;
  border: 1px solid var(--glass-border);
  border-radius: var(--pill);
  background: var(--glass-control-fill);
  color: var(--ink);
  font: inherit;
}
input::placeholder {
  color: var(--ink-quiet);
}
input:hover {
  background: var(--glass-control-fill-hover);
}
input:focus-visible,
button:focus-visible {
  outline: 2px solid var(--brand-5);
  outline-offset: 1px;
}
#site {
  flex: 1 1 24rem;
  max-width: 32rem;
}
#path {
  flex: 0 1 14rem;
}

button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: 34px;
  padding: 0 13px;
  border: 1px solid transparent;
  border-radius: var(--pill);
  background: var(--glass-control-fill);
  color: var(--glass-control);
  font: inherit;
  font-weight: 500;
  cursor: pointer;
}
button:hover {
  background: var(--glass-control-fill-hover);
}
button.icon {
  width: 34px;
  padding: 0;
}
button[aria-pressed='true'] {
  background: var(--brand-5);
  border-color: var(--brand-5);
  color: var(--white);
}
button svg {
  width: 17px;
  height: 17px;
  stroke: currentColor;
  stroke-width: 2;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}
#zoom {
  min-width: 66px;
  font-variant-numeric: tabular-nums;
}

/* ==================================================================
   The canvas. A stage that is panned rather than scrolled, the way any
   design surface is: the grid belongs to the surface and travels with it,
   so the movement is legible instead of being a picture sliding over a
   fixed backdrop.
   ================================================================== */
.canvas {
  position: absolute;
  inset: 0;
  overflow: hidden;
  cursor: grab;
  background-color: var(--ground);
  background-image:
    linear-gradient(to right, var(--grid-major) 1px, transparent 1px),
    linear-gradient(to bottom, var(--grid-major) 1px, transparent 1px),
    linear-gradient(to right, var(--grid-fine) 1px, transparent 1px),
    linear-gradient(to bottom, var(--grid-fine) 1px, transparent 1px);
}
.canvas[data-panning='true'],
.canvas[data-moving='true'] {
  cursor: grabbing;
}
/* The surface is a plane the boards stand on wherever they were put: it has
   no layout of its own, only an origin that the pan moves. */
.stage {
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  will-change: transform;
}
/* While anything is dragged the frames stop taking the pointer, so a
   gesture that crosses one keeps going instead of stopping at its edge. */
.canvas[data-panning='true'] iframe,
.canvas[data-moving='true'] iframe {
  pointer-events: none;
}

/* A station is a board and what is said about it: the caption above, and,
   when it is the one being worked on, its devices above that and its
   actions below. The rows hang from the board, so the board alone decides
   where the station stands. */
.station {
  position: absolute;
}
.station:hover {
  z-index: 100000 !important;
}
.station-head,
.station-foot {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  display: grid;
  justify-items: center;
  gap: 8px;
  width: max-content;
}
.station-head {
  bottom: 100%;
  padding-bottom: 12px;
}
.station-foot {
  top: 100%;
  padding-top: 12px;
}
.station .controls {
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.15s ease;
}
.station:hover .controls,
.station[data-selected='true'] .controls {
  visibility: visible;
  opacity: 1;
}
/* The pointer says what a press will do: the open hand pans the surface,
   the cross of arrows moves a device, the closed hand is a pan under way,
   and a screen keeps the site's own pointer, because a press there is the
   site's. */
.board {
  position: relative;
  cursor: move;
}
.board .dv-page,
.station .controls button,
.station .controls {
  cursor: auto;
}
.station .controls button {
  cursor: pointer;
}
.station-head .caption {
  cursor: move;
}
.canvas[data-moving='true'],
.canvas[data-moving='true'] .board {
  cursor: move;
}
.station[data-selected='true'] .board {
  outline: 2px solid color-mix(in srgb, var(--brand-5) 70%, transparent);
  outline-offset: 10px;
  border-radius: 18px;
}
.controls {
  display: flex;
  gap: 5px;
  align-items: center;
  flex-wrap: wrap;
  justify-content: center;
  max-width: 100%;
}
.controls button {
  height: 27px;
  padding: 0 11px;
  font-size: 12px;
  border-color: var(--glass-border);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
.controls button.icon {
  width: 27px;
  padding: 0;
}
.controls button svg {
  width: 14px;
  height: 14px;
}
.controls button[aria-current='true'] {
  background: var(--brand-5);
  border-color: var(--brand-5);
  color: var(--white);
}
/* The device picker: the browser's own list, grouped by maker. */
.controls select {
  height: 30px;
  max-width: 260px;
  padding: 0 10px;
  border: 1px solid var(--glass-border);
  border-radius: var(--pill);
  background: var(--glass-fill);
  color: var(--ink);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}
.controls select:focus-visible {
  outline: 2px solid var(--brand-5);
  outline-offset: 2px;
}
/* Two short lines, the name and the viewport, so neighbours that stand
   close do not run into each other. */
.caption {
  display: grid;
  justify-items: center;
  gap: 1px;
  margin: 0;
  color: var(--ink-quiet);
  font-size: 11px;
  font-weight: 500;
  line-height: 1.25;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.caption b {
  color: var(--ink);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* --- the device ----------------------------------------------------------
   Sizes are set by frame.js; this is only the material. A laptop is drawn
   by devices.css; a phone, a tablet and a monitor here. devices.css makes
   every element in a device a block, so `hidden` is honoured here. */
.board .device {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
}
.device [hidden] {
  display: none !important;
}
.device.dv-own {
  background: #0a0a0b;
  box-shadow:
    0 0 0 1px rgb(255 255 255 / 0.1),
    0 24px 60px -24px rgb(0 0 0 / 0.55);
}
.device.dv-own .device-screen,
.device.dv-plain .device-screen {
  overflow: hidden;
  background: #000;
}
.device.dv-plain .device-screen {
  border-radius: 6px;
  box-shadow:
    0 0 0 1px var(--glass-border),
    0 18px 48px -24px rgb(0 0 0 / 0.5);
}
.device.dv-own > :not(.device-frame),
.device.dv-plain > :not(.device-frame) {
  display: none;
}
.device .dv-page {
  position: absolute;
  overflow: hidden;
  transform-origin: 0 0;
  background: var(--white);
}
.device .dv-page iframe {
  position: absolute;
  top: 0;
  left: 0;
  border: 0;
  background: var(--white);
  transition: opacity 0.25s ease;
}
/* While the site loads, the screen is dark and breathes. */
.device .dv-page iframe:not([data-ready]) {
  opacity: 0;
}
.device .dv-page:has(iframe:not([data-ready])) {
  background: #16171a;
  animation: loading 1.4s ease-in-out infinite alternate;
}
@keyframes loading {
  to {
    background: #24262b;
  }
}
/* The system's status bar: its height is the device's, the page starts
   below it. Black, as it is over a dark page, with the time and the battery. */
.device .dv-status {
  position: absolute;
  inset: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: #000;
  color: #fff;
  font-family: ui-sans-serif, system-ui, sans-serif;
  font-weight: 600;
  letter-spacing: -0.01em;
}
.device .dv-battery {
  width: 1.6em;
  height: 0.78em;
  padding: 0.09em;
  border: 0.08em solid rgb(255 255 255 / 0.45);
  border-radius: 0.26em;
  background: #fff content-box;
}
/* A MacBook's notch sits in the menu bar, above the page this viewport is:
   drawn over the page it would hide what the page really shows. */
.device-macbook-pro .device-header {
  display: none;
}
.device .dv-cutout {
  position: absolute;
  z-index: 2;
  background: #000;
}

/* The row of the lab's own controls along the bottom. */
.adder .divider {
  height: 18px;
}

.empty {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 10px;
  padding: 96px 24px 24px;
  text-align: center;
  color: var(--ink-quiet);
  pointer-events: none;
}
.empty h1 {
  margin: 0;
  color: var(--ink);
  font-size: clamp(22px, 2.4vw, 30px);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.2;
}
.empty p {
  margin: 0;
  max-width: 34rem;
}
/* The notice lets the canvas take the pointer; only what can be pressed
   inside it takes it back. */
.empty a,
.empty button {
  pointer-events: auto;
}
.examples {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 6px;
  margin-top: 6px;
}
.examples span {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.examples button {
  border-color: var(--glass-border);
  font-variant-numeric: tabular-nums;
}
.empty .fine {
  margin-top: 14px;
  font-size: 12px;
}
.empty a {
  color: inherit;
  text-underline-offset: 2px;
}
.narrow-note {
  display: none;
}
/* An author `display` beats the browser's own rule for `[hidden]`, so the
   attribute has to be honoured here or the notice never leaves. */
.empty[hidden] {
  display: none;
}

/* Above the note line and not inside it: the note is `pointer-events:
   none` so a long trouble message never swallows a click, and a row of
   buttons in there could not be pressed. */
.adder {
  position: fixed;
  bottom: 36px;
  left: 0;
  right: 0;
  z-index: 100;
}
.adder[hidden] {
  display: none;
}

footer {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 100;
  padding: 10px 24px 14px;
  color: var(--ink-quiet);
  font-size: 12px;
  text-align: center;
  pointer-events: none;
}
footer[data-trouble='true'] {
  color: var(--danger);
  font-weight: 600;
}

/* --- presentation ---------------------------------------------------
   The bar stays. What goes is what only an operator needs — the two fields
   and the slash between them, and every board's own row of controls. What
   is left is what a viewer should see: whose tool this is, which tool it
   is, and the controls that drive the picture. */
:root[data-stage='true'] [data-stage-hide],
:root[data-stage='true'] footer,
:root[data-stage='true'] .adder,
:root[data-stage='true'] .station .controls {
  display: none;
}
:root[data-stage='true'] .group.context {
  flex: 0 0 auto;
}
:root[data-stage='true'] .caption {
  opacity: 0.7;
}
.hint {
  position: fixed;
  bottom: 18px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 210;
  padding: 7px 15px;
  border: 1px solid var(--glass-border);
  border-radius: var(--pill);
  background: var(--glass-fill);
  box-shadow: var(--glass-shadow);
  backdrop-filter: blur(18px) saturate(180%);
  -webkit-backdrop-filter: blur(18px) saturate(180%);
  color: var(--ink-quiet);
  font-size: 12px;
  transition: opacity 0.4s ease;
}
.hint[hidden] {
  display: none;
}

/* --- a narrow screen ------------------------------------------------
   The lab is a desktop tool: three devices side by side need the room.
   On a phone it still opens and still works, with the bar reduced to the
   field and the controls that matter, and it says where it is at home. */
@media (max-width: 760px) {
  header {
    gap: 8px;
    padding: 0 6px 0 12px;
  }
  .brand em,
  .chain .divider,
  .app,
  .slash,
  #path,
  #out,
  #in,
  #zoom,
  #frame,
  #stage,
  .group.actions .divider {
    display: none;
  }
  #site {
    flex: 1 1 auto;
  }
  /* The symbol alone: the same file, cropped to its left edge. */
  .brand img {
    width: 28px;
    object-fit: cover;
    object-position: left center;
  }
  .narrow-note {
    display: block;
    font-size: 12px;
  }
}
