@scope (.homelab) {
/* =============================================================================
   homelab.nithitsuki.com

   The diagram is the original 3508x2480 Krita canvas. .diagram holds that ratio
   and is scaled to fit .stage, the page column above the footer; app.js positions
   everything inside it as a percentage of the diagram, from boxes measured out of
   the .kra.

   Because the stage is a known, fixed size, `cq` units are exact: 1cq = 1% of the
   stage's width, so a value in cq is literally the artwork's own value on the
   3508px canvas, scaled. That is why borders and type are written in cq rather
   than with clamp() at every breakpoint - they track the drawing.

   Linework is redrawn here (CSS borders + one SVG), not lifted from the .kra: the
   artist drew it freehand, so those bitmap strokes came out wobbly and with
   mismatched corner joins.

   Sizing rules:
     - body      a grid: .stage takes 1fr of the height, .footer takes what it needs
     - .stage    the column above the footer; a size container that centres .diagram
     - .diagram  the artwork, aspect-ratio 3508/2480, fit to .stage in both axes
     - .footer   centred note and copyright, below the stage
     - .node     positioned by JS as % of the stage
     - borders, strokes, type  cq / cqmin, so they track the stage
   ========================================================================== */

/* ---------- tokens ------------------------------------------------------- */
:root {
  --orange: #f38020;
  --bg: #ffffff;
  --surface: #ffffff;
  --ink: #16161b;
  --rule: #16161b;
  --shadow: 0 8px 24px rgb(0 0 0 / .10);
  --shadow-lift: 0 14px 34px rgb(0 0 0 / .18);

  /* Line weight for everything the page draws itself: the tile frames, the bar, the
     tailnet box and the connector lines all use this one value, so the whole drawing
     is one hand. tools/build_geometry.py measures the artist's weight and app.js
     overwrites this with it; the number here is only the pre-script fallback.
     --rule-w is the same weight in canvas px, for the SVG, which is on the canvas'
     own coordinate system where stroke-width is not a CSS length. */
  --stroke: 0.285cqw;
  --rule-w: 10;
}

[data-theme="dark"] {
  --bg: #131418;
  --surface: #1c1e24;
  --ink: #f3f3f1;
  --rule: #e9e9e5;
  --shadow: 0 8px 24px rgb(0 0 0 / .5);
  --shadow-lift: 0 14px 34px rgb(0 0 0 / .65);
}

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  min-height: 100vh;
  min-height: 100dvh;
  padding: clamp(.4rem, 1.2vh, 1.25rem);
  background: var(--bg);
  color: var(--ink);
  font: 400 "Poppins", system-ui, -apple-system, "Segoe UI", sans-serif;
  overflow: hidden;
}

:where(button, a, [tabindex]):focus-visible {
  outline: 2px solid var(--orange);
  outline-offset: 2px;
}

/* ---------- the stage ----------------------------------------------------
   The row above the footer, and the container the artwork is fitted to. It is a
   size container so .diagram can be measured in cqw/cqh against the space that is
   actually left after the footer, rather than against the viewport. */
.stage {
  min-height: 0;
  width: 100%;
  display: grid;
  place-items: center;
  container-type: size;
}

.diagram {
  position: relative;
  aspect-ratio: 3508 / 2480;               /* the artwork's own ratio */
  /* fit the stage in both axes: its height, at most, and the height implied by
     its width, whichever is smaller; width follows from the ratio */
  height: min(100cqh, calc(100cqw * 2480 / 3508));
  width: auto;
  container-type: size;
}

/* the lists are position carriers only - no markers, no boxes */
.nodes {
  display: contents;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* ---------- the linework ---------------------------------------------------
   One SVG on the canvas' own coordinate system (viewBox 0 0 3508 2480), holding
   every line the page draws: the connector wires, the tile frames, the bar's
   outline and the tailnet box. It sits above the tiles so their frames are drawn
   over their panels, and above the bar so its outline is drawn over its fill. */
.lines {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;
  z-index: 3;
}
.lines .line {
  fill: none;
  stroke: var(--rule);
  stroke-width: var(--rule-w);
  stroke-linecap: butt;
  vector-effect: none;
}

/* ---------- the orange bar ----------------------------------------------- */
/* Fill only. The outline is a rect in the SVG with everything else's, so that the
   bar's border is the same weight as the frames and the wires. */
.tunnel {
  position: absolute;
  background: var(--orange);
  z-index: 1;
}

/* the two wordmark plates, cropped from the artwork */
.plate {
  position: absolute;
  background-size: 100% 100%;
  background-repeat: no-repeat;
}

/* ---------- the tailnet box -----------------------------------------------
   Just a hit-testing blocker over the tailnet's contents; its outline is a rect in
   the SVG along with every other line on the page. */
.tailnet {
  position: absolute;
  pointer-events: none;
  z-index: 0;
}

/* ---------- tiles -------------------------------------------------------- */
.node {
  position: absolute;
  /* an absolutely positioned <li> still computes to display:list-item and paints
     a marker unless the list style is cleared here, not just on the parent <ul> */
  display: block;
  list-style: none;
  z-index: 2;
}

.tile {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%; height: 100%;
  padding: 0; margin: 0;
  background: none;
  border: 0;
  color: inherit; font: inherit;
  text-align: center;
  cursor: pointer;
  transition: filter .18s cubic-bezier(.2, .8, .3, 1);
}
/* Hover marks the tile without moving it. A tile's outline is a rect in the shared
   SVG, so translating the tile would slide its panel out from under its own frame. */
.tile:hover, .tile:focus-visible {
  filter: brightness(.96);
}
[data-theme="dark"] .tile:hover, [data-theme="dark"] .tile:focus-visible {
  filter: brightness(1.12);
}

/* the top-row frames: the white panel is all this draws. Its outline is a rect in
   the SVG, shared with the bar, the tailnet box and the wires. */
.tile--framed {
  background: var(--surface);
}
/* Marks with black lettering that are not greyscale - jellyfin - cannot be inverted
   without shifting their brand colours, so their tile keeps a light plate in dark
   mode. This is the artwork's own background, which write_art() dropped along with
   every other page-white sheet; putting it back here is what the logo was drawn for. */
[data-theme="dark"] .tile--plate { background: #ffffff; }

/* artwork box, positioned at the artwork's own measured bounds.
   Deliberately not a centring grid: place-items:center makes the img a centred
   grid item, and a centred item's height:100% falls back to auto - which lets a
   crop taller than its box spill straight through the caption band below it. The
   img fills the box instead, so object-fit does the fitting that was intended. */
.tile__art {
  position: absolute;
  display: block;
}
/* object-fit: fill - each crop is already cut to exactly the box geometry hands
   it, so stretching it back is what restores the original scale */
.tile__art img {
  display: block;
  width: 100%; height: 100%;
  object-fit: fill;
}
/* ...except on tiles that reserve a caption band: those fit the artwork into the
   space above the band so the label never overlaps the logo */
.tile__art--fit img { object-fit: contain; }
.tile__vector { width: 100%; height: 100%; object-fit: contain; }

/* Captions. Each is confined to a band geometry reserved for it - the lower part
   of a top-row frame, or the strip under a device photo - so a caption can never
   land on a logo. top/height/font-size are all set inline from the artwork's own
   measurements, as percentages of this tile. */
.tile__cap {
  position: absolute;
  left: 0; right: 0;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.01em;
  white-space: nowrap;
}
.tile__cap--under { color: var(--ink); }
/* A machine's label colour, sampled out of the .kra on its white page. */
.tile__cap--strip { color: var(--label, var(--ink)); }
/* Those samples are mostly dark inks, so on a dark page they are lightened towards
   white rather than replaced - the hue still says which machine it is. */
[data-theme="dark"] .tile__cap--strip {
  color: color-mix(in oklab, var(--label, #f3f3f1) 40%, #f3f3f1);
}

/* Greyscale line art, black in the artwork. Inverted on a dark page so it stays
   visible; coloured marks are left alone so their brand colour survives. */
[data-theme="dark"] .tile__art--mono { filter: invert(1); }

/* ---------- theme toggle -------------------------------------------------- */
.theme-toggle {
  position: fixed; top: 1rem; right: 1rem; z-index: 40;
  display: grid; place-items: center;
  width: 2.4rem; height: 2.4rem; padding: 0;
  background: var(--surface); color: var(--ink);
  border: 2px solid var(--rule); border-radius: 50%;
  cursor: pointer;
  transition: transform .18s ease, box-shadow .18s ease;
}
/* A lift rather than a spin: the sun and moon already rotate into place, so a
   turning button on top of that reads as two competing motions. */
.theme-toggle:hover { transform: scale(1.06); box-shadow: var(--shadow); }

/* Both glyphs sit in the same 20px box and cross-fade; app.js writes the resolved
   theme to [data-theme], so the document element alone picks which one is shown. */
.theme-toggle__icon {
  position: relative;
  display: block;
  width: 20px; height: 20px;
}
.icon {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  transform-origin: 50% 50%;
  transition: opacity .3s ease, transform .45s cubic-bezier(.2, .8, .3, 1);
}
.icon--sun { opacity: 1; transform: rotate(0deg) scale(1); }
.icon--moon { opacity: 0; transform: rotate(-90deg) scale(.55); }
[data-theme="dark"] .icon--sun { opacity: 0; transform: rotate(90deg) scale(.55); }
[data-theme="dark"] .icon--moon { opacity: 1; transform: rotate(0deg) scale(1); }

/* ---------- footer -------------------------------------------------------
   A quiet credit line under the drawing. It is a body grid item, so it takes its
   own height and the stage above it shrinks by exactly that much. */
.footer {
  justify-self: center;
  max-width: 100%;
  display: flex; flex-wrap: wrap; justify-content: center; align-items: baseline;
  gap: .25rem .6rem;
  margin: 0;
  padding: .35rem .5rem .15rem;
  font-size: clamp(.7rem, 1vw, .82rem);
  line-height: 1.4;
  color: var(--ink);
  opacity: .62;
  text-align: center;
}
.footer__note, .footer__copy { margin: 0; }
.footer a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: .18em;
  text-decoration-thickness: 1px;
}
.footer a:hover { color: var(--orange); opacity: 1; }

/* ---------- click panel -------------------------------------------------- */
.drawer { position: fixed; inset: 0; z-index: 60; }

.drawer__scrim {
  position: absolute; inset: 0;
  background: rgb(0 0 0 / .45);
  animation: fade .2s ease both;
}

.drawer__panel {
  position: absolute; inset-block: 0; right: 0;
  width: min(24rem, 100%);
  display: flex; flex-direction: column; align-items: flex-start; gap: .5rem;
  padding: clamp(1.4rem, 4vw, 2.2rem);
  background: var(--surface);
  border-left: 2px solid var(--rule);
  box-shadow: -22px 0 50px rgb(0 0 0 / .25);
  overflow-y: auto;
  animation: slide-in .24s cubic-bezier(.2, .8, .3, 1) both;
}

.drawer__close {
  position: absolute; top: 1rem; right: 1rem;
  display: grid; place-items: center;
  width: 2.2rem; height: 2.2rem;
  background: none; color: var(--ink);
  border: 2px solid var(--rule); border-radius: 50%;
  cursor: pointer;
}

.drawer__mark {
  display: grid; place-items: center;
  width: 3.4rem; height: 3.4rem;
  margin-bottom: .3rem;
}
.drawer__mark img { width: 100%; height: 100%; object-fit: contain; }

.drawer__name { font-size: 1.5rem; font-weight: 600; }
.drawer__blurb { margin: 0; opacity: .78; }

.drawer__cta {
  margin-top: .5rem;
  padding: .45rem 1rem;
  background: var(--orange); color: #fff;
  border: 2px solid var(--rule); border-radius: 2px;
  font: inherit; font-weight: 600; text-decoration: none;
}

@keyframes fade { from { opacity: 0 } }
@keyframes slide-in { from { transform: translateX(100%) } }

/* One canvas at every width: on phones the drawing scales with the stage instead
   of reflowing into a stacked list. The labels get small; that is deliberate. */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    transition-duration: .001ms !important;
  }
}
}