/* ---------- faces ----------
   Self-hosted, not fetched from a font CDN: this box already refuses to make
   outbound calls on a visitor's behalf everywhere else, and the pages should
   not be the exception. Latin subset only -- 46 KB for all four, against
   several hundred for the full families. Licences sit beside the files.

   Chakra Petch carries the two hub pages. Michroma carries one word on the
   refusal page and nothing else, which is the only place a face that wide and
   that mannered is a good idea. */

@font-face {
  font-family: 'Chakra Petch';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('assets/fonts/chakra-petch-400.woff2') format('woff2');
}
@font-face {
  font-family: 'Chakra Petch';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('assets/fonts/chakra-petch-500.woff2') format('woff2');
}
@font-face {
  font-family: 'Chakra Petch';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('assets/fonts/chakra-petch-600.woff2') format('woff2');
}
/* Inter and JetBrains Mono are for the tooltips only. Chakra Petch is a
   display face: it carries a 380px letterform and a tracked-out label very
   well, and at 11px in a dense card it reads as game UI no matter what the
   padding does. Small supporting text wants a text face, and a hostname wants
   a mono -- that is what makes the card look like tooling rather than a HUD. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('assets/fonts/inter-400.woff2') format('woff2');
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('assets/fonts/inter-600.woff2') format('woff2');
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('assets/fonts/jetbrains-mono-400.woff2') format('woff2');
}
@font-face {
  font-family: 'Michroma';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('assets/fonts/michroma-400.woff2') format('woff2');
}

/* One stack, named once. Chakra Petch has no italic and no other weights here,
   so anything asking for one falls through to the system UI face. */
:root {
  --face: 'Chakra Petch', ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --ui: 'Inter', ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono: 'JetBrains Mono', ui-monospace, "Cascadia Mono", Menlo, Consolas, monospace;
}

/* Shared by index.html and tools.html: the two pages are the same drawing with
   different satellites, so the drawing lives here once. */

:root {
  --ink: #00b4ff;
  --line: rgba(0, 168, 255, 0.34);
  /* Cool silver, so the person reads as a different kind of thing from
     the projects rather than as another one of them. */
  --silver: #c6ccd6;
  /* One number drives the whole composition and, through calc() below, the
     glow -- so the halo keeps its proportions at every size instead of going
     woolly on a phone and thin on a monitor. */
  --size: clamp(20rem, 96vmin, 64rem);
}

html, body { height: 100%; }
body {
  margin: 0;
  background: #0b0d10;
  display: grid;
  place-items: center;
}

.hub {
  width: var(--size);
  height: auto;
  max-height: 92vh;
  /* Load-bearing, not tidy-up-able. An SVG viewport clips its children, so
     without this the halo a satellite grows on hover is sliced off flat at
     the element's edge. */
  overflow: visible;
}

/* ---------- the mark at the centre ---------- */

.mark { fill: var(--ink); }
/* Back, but a third of what it was. Scaled off --size so the halo keeps its
   proportion at every size instead of going woolly on a phone and thin on a
   monitor. It only survives because .hub sets overflow:visible: an SVG viewport
   clips a child's filter, and this one is on a child. */
.mark__core { filter: drop-shadow(0 0 calc(var(--size) * 0.013) rgba(0, 180, 255, 0.5)); }

/* ---------- ring and connectors ---------- */

.ring, .link path {
  fill: none;
  stroke: var(--line);
  stroke-width: 2;
}
.ring { stroke-width: 2.5; }
/* Mitred, not rounded: the corner where the diagonal meets the straight run is
   the whole point of the shape, and a round join softens it into a curve. */
.link path { stroke-linecap: round; stroke-linejoin: miter; }

/* Sector dividers. Dashed and fainter than the connectors, because they carry
   no traffic: they say where one group of things stops and the next begins. */
.divider {
  fill: none;
  stroke: var(--line);
  stroke-width: 1.5;
  stroke-dasharray: 5 9;
  stroke-linecap: round;
  opacity: 0.55;
}

/* ---------- the satellites ---------- */

.node__disc {
  fill: #0e1319;
  stroke: var(--line);
  stroke-width: 2;
}
/* Its own class rather than .node__disc with fill="none": a CSS rule beats a
   presentation attribute, so the fill would have won and painted an opaque
   disc straight over the photo. */
.node__rim {
  fill: none;
  stroke: var(--line);
  stroke-width: 2;
}
.node__icon { color: var(--ink); fill: currentColor; opacity: 0.9; }
/* Icons drawn in line rather than solid opt out HERE, not with a fill="none"
   attribute on the element. A CSS rule beats a presentation attribute, so the
   attribute lost and every ring, box and cylinder came out as a filled blob:
   the same trap as .node__rim below, one element deeper. */
.node__icon--line {
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Faint on purpose: they name the discs without competing with them. */
.node__label {
  fill: var(--ink);
  opacity: 0.45;
  font: 500 13px/1 var(--face);
  letter-spacing: 0.12em;
  text-anchor: middle;
  dominant-baseline: auto;
  /* The labels are named by <title> already; reading them twice adds nothing. */
  user-select: none;
}
.node__label--right { text-anchor: start; }
.node__label--name {
  fill: var(--silver);
  opacity: 0.8;
  font-size: 15px;
  letter-spacing: 0.06em;
}
.node__label--sub {
  fill: var(--silver);
  opacity: 0.42;
  font-size: 10.5px;
  letter-spacing: 0.05em;
  font-weight: 400;
}
a.node:hover .node__label,
a.node:focus-visible .node__label { opacity: 0.9; }

a.node { cursor: pointer; }
a.node .node__disc,
a.node .node__rim,
a.node .node__label,
a.node .node__icon { transition: stroke 180ms ease, opacity 180ms ease, filter 180ms ease; }
a.node:hover .node__disc,
a.node:hover .node__rim,
a.node:focus-visible .node__disc,
a.node:focus-visible .node__rim {
  stroke: var(--ink);
  filter: drop-shadow(0 0 calc(var(--size) * 0.02) rgba(0, 180, 255, 0.75));
}
a.node:hover .node__icon,
a.node:focus-visible .node__icon { opacity: 1; }
a.node:focus { outline: none; }

/* Not yet built. Dimmed rather than hidden, because the shape of what is
   coming is part of what this page says.

   The dimming is applied to the parts, not to the group: on the group it would
   have taken the tooltip down with it, and these still answer to a hover even
   though there is nowhere to go. They are <g>, not <a>, so there is nothing to
   click and no highlight rule matches them. */
.node--soon .node__disc,
.node--soon .node__rim,
.node--soon .node__icon,
.node--soon .node__label { opacity: 0.3; }
.node--soon { cursor: default; }

/* ---------- tooltips ----------
   The browser's own arrives late, in the system font, at the pointer rather
   than at the thing. These arrive immediately, in the page's face, over the
   disc they belong to.

   On the internal page each is a small card: a coloured badge naming the
   product, the hostname under it, and a line saying what the thing actually
   does. One colour per service, so the badge is recognisable before the word
   is read. SVG will not lay a background out around a string, so every width
   here is measured, not guessed. */
/* hub.js drives these: it shows the card and moves it with the pointer, so
   there is no :hover rule here to fight with. pointer-events off matters --
   a card that sat under the cursor would take the pointer off the disc and
   flicker itself out of existence. */
.tip {
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms ease;
}
.tip.is-on { opacity: 1; }

/* one-line form, used on the front page */
.tip__box {
  fill: #12161c;
  stroke: rgba(230, 237, 243, 0.10);
  stroke-width: 1;
}
.tip__text {
  fill: #d0d7de;
  font: 500 11.5px/1 var(--ui);
  text-anchor: middle;
}

/* card form, used on the internal page */
.tip__card {
  fill: #12161c;
  stroke: rgba(230, 237, 243, 0.10);
  stroke-width: 1;
}
/* fill is set per pill: dark on a solid segment, the service colour on a
   muted one, exactly as a scoped label is set */
.tip__pill {
  font: 600 11px/1 var(--ui);
  text-anchor: middle;
}
.tip__host {
  fill: #6e7781;
  font: 400 10px/1 var(--mono);
}
.tip__desc {
  fill: #b1bac4;
  font: 400 11.5px/1 var(--ui);
}
.tip__text, .tip__pill, .tip__host, .tip__desc { user-select: none; }

