/* Node-zoom navigation: the camera flies into the clicked graph node, and the
   target page is already laid out inside it. See src/node-zoom.js.

   Everything that moves here is a transform or an opacity, so the whole zoom
   stays on the compositor and never repaints. */

/* The node's colour, and how much of it the page surface keeps. 100% is the
   node colour exactly; lower values darken it towards black, which the white
   body type needs to stay readable.

   These live on :root rather than on the overlay because a subpage opened
   directly has no overlay - it paints the node colour on its own body. The
   overlay still sets --nz-color inline per node, which wins over both. */
:root {
    --nz-color: #fff;
    --nz-tint: 100%;
}

#node-zoom {
    position: fixed;
    inset: 0;
    z-index: 999;
    pointer-events: none;
}

#node-zoom[hidden] {
    display: none;
}

#node-zoom.nz-settled {
    pointer-events: auto;
}

/* The node itself. Laid out at DISC_RADIUS and scaled from there, so the edge
   is rasterised from a decent number of pixels rather than from a dot. */
.nz-disc {
    position: absolute;
    width: 240px;
    height: 240px;
    margin: -120px 0 0 -120px;
    border-radius: 50%;
    background: var(--nz-color);
    transform-origin: 50% 50%;
    will-change: transform;
}

/* The page, laid out at full size from the first frame and seen from far away.
   Its corners ride the disc's edge at every magnification, so the disc is
   always exactly the colour framing it - no clipping involved.

   The scrollbar gutter is reserved from the start: letting a scrollbar appear
   at the end of the zoom would reflow the content on the final frame. */
.nz-page {
    position: absolute;
    inset: 0;
    overflow-y: auto;
    overflow-x: hidden;
    scrollbar-gutter: stable;
    -webkit-overflow-scrolling: touch;
    background: color-mix(in srgb, var(--nz-color) var(--nz-tint), #000);
    will-change: transform;
}

/* Focused on open so the keyboard lands inside the overlay; a ring would be
   noise on a full-bleed surface that is plainly already the whole screen. */
.nz-page:focus {
    outline: none;
}

.nz-content {
    min-height: 100vh;
}

.nz-close {
    position: fixed;
    top: 18px;
    right: 22px;
    z-index: 2;
    opacity: 0;
    pointer-events: none;
    width: 36px;
    height: 36px;
    padding: 0;

    background: none;
    border: 1.5px solid #fff;
    color: #fff;
    cursor: pointer;
    transition: opacity 200ms ease-out, background-color 150ms ease-out,
        color 150ms ease-out;
}

/* The cross is drawn, not typed. U+00D7 sits on the font's math axis, which is
   above the centre of the em box, so a text glyph rides high however the box is
   centred - and it shifts again with the font. Two bars centred by `inset: 0;
   margin: auto` are exact by construction and owe nothing to the typeface. */
.nz-close::before,
.nz-close::after,
.nz-home::before,
.nz-home::after {
    content: "";
    position: absolute;
    inset: 0;
    margin: auto;
    width: 16px;
    height: 1.5px;
    background: currentColor;
}

.nz-close::before,
.nz-home::before {
    transform: rotate(45deg);
}

.nz-close::after,
.nz-home::after {
    transform: rotate(-45deg);
}

/* The close button belongs to the page, not to the node, so it only arrives
   once the camera has come to rest. */
#node-zoom.nz-settled .nz-close {
    opacity: 1;
    pointer-events: auto;
}

/* Inverting borrows the node's own colour for the glyph, so the button reads as
   part of the page. A fixed accent could not: teal sat well on black but
   clashed with whatever colour the overlay happened to be painted.
   Focus shares the hover look rather than adding a ring: the button is only
   focused when someone has actually tabbed to it, and a ring around an already
   bordered box read as two nested squares. */
.nz-close:hover,
.nz-close:focus-visible {
    background: #fff;
    color: var(--nz-color);
    outline: none;
}

/* Links inside the overlay sit on the node's colour, where the site's teal
   accent measures 1.06:1 to 1.27:1 - invisible, and on the teal node it is
   teal on teal. No flat colour fixes it either: white fails on the orange
   (3.76:1) and black fails on the deep blue (3.72:1), so any single choice
   breaks one page.

   The graph already answers this. Hovering a node label swaps it to a white
   box with black text; the same gesture here is legible on every colour by
   construction, and it is a hover state the site has already taught.

   The inset is drawn with a spread shadow rather than padding so that
   highlighting an inline link cannot reflow the paragraph around it.

   The same page opened directly needs the identical treatment, and for the
   identical reason: it is painted its node's colour whether or not an overlay
   is what put it there, so scoping this to #node-zoom left every link on a
   cold-loaded subpage falling back to the site's teal, on the one background
   teal cannot be read on. Keyed on [data-node] rather than on .nz-inside,
   because a page with no node of its own is still black, where the site's own
   accent is exactly right - and .nz-home is excluded, having its own
   inversion just below. */
#node-zoom a:hover,
#node-zoom a:focus-visible,
body.nz-inside[data-node] a:hover:not(.nz-home),
body.nz-inside[data-node] a:focus-visible:not(.nz-home) {
    background: #fff;
    color: #000;
    border-radius: 3px;
    outline: none;

    /* One painted background, not two abutting ones. A spread box-shadow is
       clipped to outside the border box, so it met the background edge to edge
       and the two antialiased seams did not sum to full coverage - the page
       colour bled through as a hairline ring around the highlight.

       Padding is safe here because these are inline links: vertical padding on
       an inline box does not grow the line, and the negative side margins
       cancel the horizontal padding, so nothing reflows on hover. */
    padding: 4px 5px;
    margin: 0 -5px;
}

/* ------------------------------------------------------- a page on its own */

/* A subpage reached directly - from a search result, a shared link, a refresh
   - was never flown into, but it is still the inside of its node. Painting the
   node's colour on the body statically means the hand-off back to the front
   page has no colour to cross: the overlay that catches it there is already
   painted the same value, so the navigation has nothing to flash.

   The colours are the ones the graph draws its nodes with, in
   data/network_data.json. CSS cannot read that file, so this is the one place
   they are written twice; keep the two in step. Pages name their node rather
   than their colour, so a blog post and the blog index cannot drift apart, and
   a page with no node of its own simply keeps the site's black. */
body[data-node="about"],
body[data-node="egg"] {
    --nz-color: rgb(189, 113, 56);
}

body[data-node="contact"] {
    --nz-color: rgb(170, 76, 219);
}

body[data-node="projects"] {
    --nz-color: rgb(64, 130, 118);
}

body[data-node="blog"] {
    --nz-color: rgb(62, 99, 189);
}

body.nz-inside {
    background: color-mix(in srgb, var(--nz-color) var(--nz-tint), #000);
}

/* The other end of that hand-off. The front page holds the colour the reader
   was just looking at until its overlay is ready to take over, and it has to
   set that before <body> exists - so the colour goes on the root element and
   the body's own black steps out of the way, rather than painting over it. */
html.nz-arriving body {
    background: transparent;
}

/* And nothing of the front page shows through it. The colour alone was not
   enough: it is painted on the root, so every element the front page has
   already rendered sits on top of it - which meant the footer appearing on a
   blank orange field while the overlay was still being fetched.

   Dropped the moment the camera has taken the scene over, which it does by
   setting an inline opacity of 0 on all three layers - so by the time this
   goes, there is nothing here that could become visible. */
html.nz-holding .content-container {
    visibility: hidden;
}

/* The way out. It is a real link in the markup rather than something the
   script inserts, so a reader without JS - or one who arrives before it runs -
   is never stranded on a page with no route home. The zoom is the
   enhancement; the escape route is not.

   Same cross, same box as the overlay's button, and deliberately in the same
   place: whether the page was flown into or opened cold, the way out is under
   the same pixel. */
.nz-home {
    position: fixed;
    top: 18px;
    right: 22px;
    z-index: 2;
    display: block;
    width: 36px;
    height: 36px;

    border: 1.5px solid #fff;
    color: #fff;
    cursor: pointer;
    transition: background-color 150ms ease-out, color 150ms ease-out;
}

/* Inverting, not the site's teal hover: teal on the node's own colour is the
   same unreadable pairing the overlay's links had to solve. */
.nz-home:hover,
.nz-home:focus-visible {
    background: #fff;
    color: var(--nz-color);
    outline: none;
}

/* Touch. Both buttons are 36px, which is a comfortable mouse target and an
   awkward thumb one - and on a phone the way home is the only control the
   page has, so missing it is the whole navigation. The cross inside is
   centred by `inset: 0; margin: auto`, so growing the box does not move it. */
@media (pointer: coarse) {
    .nz-close,
    .nz-home {
        width: 44px;
        height: 44px;
        top: 12px;
        right: 12px;
    }
}
