/*
 * globe.css
 *
 * The globe lives inside #globe-stage and knows nothing about the window. The
 * canvas fills that container, and globe.js measures the container (not the
 * viewport) to size its drawing buffer and to fit the planet. Resizing the
 * stage by any means, whether these variables, a parent layout, a flex or grid
 * cell, or a JavaScript style change, is enough on its own: the canvas follows
 * it and the planet refits.
 *
 * The two variables below are the intended way to shrink the stage later. They
 * are the only place its size is decided.
 *
 * The dashboard panels deliberately do not touch them. They are laid over the
 * page rather than beside it, so that showing and hiding them cannot resize the
 * stage, and therefore cannot resize the planet or discard a zoom that was set
 * by hand. See the head of panels.css.
 */

/*
 * The palette and the two typefaces are the resume's, so the dashboard reads as
 * part of the same site rather than as a separate thing that happens to be
 * hosted next to it. The values are copied from assets/css/main.css one level
 * up, where they were sampled off a reference PCB photograph rather than picked
 * by eye. They are duplicated here rather than shared because this directory is
 * self-contained by design: it has no build step, no dependencies, and is meant
 * to run wherever it is dropped. If the resume's greens ever move, these are the
 * ones to move with them.
 */
:root {
    --globe-stage-width: 100vw;
    --globe-stage-height: 100vh;

    --page-background-colour: #000000;

    --bg-panel: #1c1815;
    --bg-elevated: #2c2724;

    --accent-primary: #00b060;
    --accent-secondary: #17c96b;
    --accent-dim: #045c28;

    --text-primary: #d7ecd3;
    --text-secondary: #9fcf9a;

    /*
     * The one hue on this page that is not from the resume's palette, and it
     * is here to do a job rather than to decorate: the map has land and water
     * on it, only the land carries borders, and a sea name set in the same
     * green as a country name reads as a country whose outline failed to draw.
     *
     * Desaturated enough to sit with the greens instead of shouting over them,
     * and light enough to stay readable over both the ocean texture and the
     * night side, where it lands around 7:1 against the darkest water.
     */
    --label-water: #8fb8d8;

    --border-subtle: #0e6b2c;

    --font-mono: 'JetBrains Mono', Consolas, 'Courier New', monospace;
    --font-console: 'VT323', Consolas, monospace;
}

/*
 * Both faces are served from this directory rather than from Google, which is
 * where the resume gets them. The resume is a page on a website; this is a
 * self-contained folder that is supposed to run from a copy with no network,
 * and a hotlinked font would quietly break that the first time it was opened
 * offline. JetBrains Mono is shipped as its variable file, which is why one
 * woff2 covers the whole 400 to 700 range.
 *
 * swap rather than block: the globe is the point of the page and should not
 * wait on typography to appear.
 */
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('../fonts/jetbrains-mono-400-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+2074, U+20AC, U+2122,
        U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/*
 * There is no Latin Extended face here, and that was checked rather than
 * assumed. The country names carry plenty of diacritics, but every one of them
 * is inside Latin-1 and so inside the block above: Côte d'Ivoire, Curaçao,
 * São Tomé and Åland all live under U+00FF. Nothing in the data set reaches
 * past it, so a Latin Extended file would be 11 KB that never renders a glyph.
 *
 * If the borders are ever rebuilt from a finer Natural Earth set and a name
 * does reach past U+00FF, that word will fall back mid-label to whatever the
 * system offers, which shows as a change of typeface inside one name. The fix
 * then is one more woff2 and one more block, not a change to anything else.
 */
@font-face {
    font-family: 'VT323';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url('../fonts/vt323-400-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+2074, U+20AC, U+2122,
        U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

html,
body {
    margin: 0;
    padding: 0;
    width: 100%;
    height: 100%;
    background-color: var(--page-background-colour);
    overflow: hidden;
}

/*
 * The stage is the box the planet is fitted into. It is positioned so that the
 * absolutely positioned canvas inside it resolves against the stage rather than
 * against the page, and it hides overflow so that nothing the canvas draws can
 * ever escape the box.
 */
#globe-stage {
    position: relative;
    width: var(--globe-stage-width);
    height: var(--globe-stage-height);
    overflow: hidden;
    background-color: var(--page-background-colour);
}

/*
 * The canvas is taken out of the normal flow so that it can never introduce a
 * scrollbar, and so that the browser never adds the few pixels of inline layout
 * leading that an inline level canvas would otherwise get. Its width and height
 * attributes are set in JavaScript; these CSS rules only stretch the element to
 * the stage.
 */
#globe-canvas {
    position: absolute;
    top: 0;
    left: 0;
    display: block;
    width: 100%;
    height: 100%;

    /*
     * touch-action: none stops a touch drag being taken by the browser as a
     * scroll or a pinch before the page ever sees it, which would make the
     * globe unusable on a touchscreen. The two user-select rules stop a mouse
     * drag turning into a text selection of the rest of the page.
     */
    touch-action: none;
    -webkit-user-select: none;
    user-select: none;
    cursor: grab;
}

/*
 * Added by globe.js for the duration of a drag. Purely the standard pointer
 * feedback for a draggable object; nothing about the rendering depends on it.
 */
#globe-canvas.is-dragging {
    cursor: grabbing;
}

/*
 * Labels
 *
 * Country and body names are HTML sitting over the canvas rather than glyphs
 * drawn into it. Text in WebGL means either a glyph atlas and a quad per
 * character or signed distance fields, and both of those are a lot of machinery
 * to end up with worse type than the browser sets for free. The browser also
 * hints and subpixel-renders at whatever the device pixel ratio happens to be,
 * which a texture atlas baked at one size does not.
 *
 * The cost is that labels cannot be occluded by the globe's depth buffer, so
 * globe.js hides them by geometry instead: a label whose point has turned away
 * from the camera is not drawn.
 *
 * The layer takes no pointer events at all, so dragging and scrolling still
 * reach the canvas underneath even with the cursor over a name.
 */
#globe-labels {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    overflow: hidden;
    pointer-events: none;
}

/*
 * Each label is positioned by a transform rather than by top and left, so that
 * moving it is a compositor operation and does not invalidate layout. The
 * translate(-50%, -50%) centres the box on its point, and globe.js appends the
 * pixel translation in front of it every frame.
 *
 * The text shadow is doing real work rather than decoration: names sit over
 * coastlines, ice, cloud and open ocean within a single frame, and a plain
 * light glyph disappears against the bright side of the planet.
 */
.globe-label {
    position: absolute;
    top: 0;
    left: 0;
    display: none;
    white-space: nowrap;
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.06em;
    color: var(--accent-secondary);

    /*
     * The same trace green as the body names, so everything written over the
     * scene is one voice.
     *
     * The black layers underneath are doing more work here than they do on the
     * body names, and are kept at full strength rather than traded for more
     * glow. A country name is 11px, it sits directly on the surface rather than
     * out in empty space, and in one frame it can cross bright desert, ice,
     * cloud and dark ocean. Green on sunlit sand is the hard case, and what
     * saves it is the dark halo, not the colour.
     */
    text-shadow:
        0 0 3px rgba(0, 0, 0, 0.95),
        0 0 6px rgba(0, 0, 0, 0.85),
        0 0 10px rgba(0, 0, 0, 0.65),
        0 0 14px rgba(0, 176, 96, 0.3);
    will-change: transform, opacity;
}

/*
 * Ocean names.
 *
 * A map has always drawn water differently from land, and the reason is not
 * decoration: the names of the seas sit over an area rather than over a place,
 * and if they are set the same as the countries they read as one more country.
 * The convention is lighter, wider and quieter, which is what this is: the pale
 * mint the resume uses for secondary text instead of the borders' trace green,
 * tracked well out, and small.
 *
 * Not uppercase, because at this size and this tracking the words are already
 * doing enough and full capitals in a monospace face read as a heading.
 */
/*
 * Water is blue, and it is the one deliberate break from the palette on the
 * whole page.
 *
 * Everything else here is the resume's greens, sampled off a circuit board,
 * and the point of that is that the dashboard reads as part of the same site.
 * Water gets its own hue anyway, because the map has two halves and only one
 * of them has borders drawn on it: a sea name set identically to a country
 * name reads as a country whose outline failed to load. Colour is doing work
 * here rather than decorating, which is the test everything else on this page
 * is held to.
 *
 * It is a desaturated slate blue rather than a saturated one, so it sits with
 * the greens instead of shouting over them, and it is light enough to clear
 * 4.5:1 against both the ocean texture and the night side.
 */
.globe-label.is-ocean,
.globe-label.is-water {
    color: var(--label-water);
}

.globe-label.is-ocean {
    font-size: 11px;
    letter-spacing: 0.28em;
}

/*
 * Seas, gulfs and bays: the same blue, set smaller and tighter than the
 * oceans, because they name a smaller thing and the difference in tracking is
 * what stops a screen carrying both from reading as one flat list of names.
 */
.globe-label.is-water {
    font-size: 10px;
    letter-spacing: 0.16em;
}

/*
 * The shipping choke points. Slightly tighter again and not letterspaced out,
 * because these are the narrowest things on the map and a name spread wide
 * across one would overhang the strait it is naming by a continent.
 */
.globe-label.is-waterway {
    font-size: 9px;
    letter-spacing: 0.06em;
}

/*
 * Capitals: one per country, and no other cities.
 *
 * This is a synoptic dashboard - it reports on the planet, and the unit it
 * reports in is the country - so the capital is here as the point that stands
 * for a country rather than as a place in its own right. A second city in the
 * same country would be a level of detail below anything the panels can say,
 * and drawing all 1,251 populated places, which is what this used to do, made
 * the globe a road atlas that happened to be round.
 *
 * A point wants a mark as well as a name: without one the text floats over a
 * piece of coastline with nothing saying which pixel it means. That is also
 * the reason for keeping the dot rather than dropping it - every other name on
 * this globe labels an *area*, where centring the text over the shape is
 * right, and a capital is the only thing labelling a *point*. The dot is what
 * says so.
 *
 * The dot sits above the name and dead on the point, and getting there needs
 * the same arithmetic the focus marker needed. placeLabel centres every label
 * on its coordinates with translate(-50%, -50%), so a dot pinned to the left
 * edge of the box - which is what this used to be - lands half the element's
 * width to the *left* of the city. Worse, the width depends on the length of
 * the name, so the error was different for every capital: a couple of pixels
 * for Lima and about thirty-five for Washington, D.C.
 *
 * So the dot goes at the middle of the box, which is the point, and the top
 * padding pushes the name down clear of it. The box is padding-top plus
 * line-height tall and the dot is at half of that; the two stay in step
 * because the line height is stated rather than inherited, and the padding is
 * larger than it so the text starts below the dot rather than through it.
 *
 * Set in the resume's pale mint rather than the trace green the country names
 * use, so the hierarchy is legible without a second hue: the country is the
 * brighter thing, its capital the quieter one inside it.
 */
.globe-label.is-capital {
    padding-top: 16px;
    font-size: 9px;
    line-height: 10px;
    letter-spacing: 0.04em;
    color: var(--text-primary);
    opacity: 0.9;
}

.globe-label.is-capital::before {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 3px;
    height: 3px;
    margin-top: -1.5px;
    margin-left: -1.5px;
    border-radius: 50%;
    background: var(--text-primary);
    opacity: 0.8;
}

/*
 * The numbers on the grid.
 *
 * These are the grid's own colour, the primary trace green rather than the
 * brighter secondary the country names use, so they read as part of the lines
 * they are numbering rather than as more names on the map. Smaller than
 * anything else on the globe as well: a scale should be readable when it is
 * looked for and invisible when it is not.
 */
.globe-label.is-grid-value {
    font-size: 9px;
    letter-spacing: 0.05em;
    color: var(--accent-primary);
}

/*
 * The focus marker: where a dashboard panel row was last asked to point.
 *
 * The only label on the globe that is not a fact about the map or the sky, so
 * it is the only one allowed to be loud. Everything else written over the
 * planet is recessive by design, because a name is there to be read when it is
 * looked for and to stay out of the way when it is not. This one is the answer
 * to a question asked a second ago, and it has to be findable on a planet the
 * viewer has just watched swing across the screen.
 *
 * It carries no text. It used to print the name of whatever was clicked, which
 * was right while the globe was unlabelled and a red dot on empty ocean meant
 * nothing on its own. Now that the seas, the waterways and the cities are
 * named on the planet permanently, the name is already there, and printing it
 * again in a different colour next to itself was the marker arguing with the
 * map rather than pointing into it.
 *
 * Being a dot and nothing else also makes it easy to place. Every label on
 * this layer is positioned by translate(-50%, -50%), which centres the element
 * on the point it names; with no text in the box, the centre of the element is
 * the centre of the dot, and the dot lands exactly on the place.
 *
 * The ring and the glow are box-shadow rather than extra elements, so the
 * whole mark is one 9 pixel box however loud it looks.
 */
.globe-label.is-focus {
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--status-critical, #e0533d);
    box-shadow:
        0 0 0 3px rgba(224, 83, 61, 0.30),
        0 0 12px 3px rgba(224, 83, 61, 0.60);
}

/*
 * The three bodies get the console face, at a larger size and letterspaced
 * wide. The resume reserves VT323 for short atmospheric labels where the retro
 * console feel matters more than reading at length, and SOL, TERRA and LUNA are
 * exactly that: three words, never read as prose, and wanting to look like
 * something a terminal printed rather than something a map did.
 */
.globe-label.is-body {
    font-family: var(--font-console);
    font-size: 20px;
    letter-spacing: 0.34em;
    text-transform: uppercase;
    color: var(--accent-secondary);
    text-shadow:
        0 0 4px rgba(0, 0, 0, 0.95),
        0 0 10px rgba(0, 176, 96, 0.35);
}

/*
 * Controls
 *
 * Top middle, above the canvas and above the labels. Deliberately plain: this
 * is the minimum that is legible over a black sky and a bright planet, and the
 * look is not meant to be the final word on it.
 *
 * The row is a sibling of the stage rather than a child of it, and fixed to the
 * page rather than positioned inside it. It is chrome for the whole dashboard
 * now, not a label on the globe: one of its buttons operates the panels, which
 * are not in the stage either. It also cannot be clipped there. The stage hides
 * its overflow, so any future change that makes the stage smaller than the
 * window would cut the ends off the row, and that is a trap worth not leaving
 * set. Nothing in globe.js looks the buttons up through the stage, so the move
 * costs nothing.
 */
/*
 * The row is a measured bar rather than a huddle of buttons at whatever width
 * their labels happen to come to: the full page width on a screen higher than
 * it is wide, and a third of the page - the same third the globe sits in
 * between the panel rails - on a screen wider than it is high. The buttons are
 * equal columns of it rather than each its own size, so the bar reads as one
 * object.
 *
 * box-sizing is set here because this file does not carry a global reset and
 * the width is now stated in viewport units. Without it the padding and the
 * border would be added on top of 100vw and the bar would be wider than the
 * page it is meant to fit.
 */
#globe-controls {
    box-sizing: border-box;
    position: fixed;
    top: 16px;
    left: 50%;
    transform: translateX(-50%);
    width: 100vw;

    /*
     * The bar is two stacked rows: the heading and the way back on top, the
     * buttons under it. The grid that makes the buttons equal columns lives on
     * the button row rather than here, so the heading spans the bar without
     * having to be told to span anything.
     */
    display: flex;
    flex-direction: column;

    gap: 8px;
    padding: 8px;
    background-color: var(--bg-panel);

    /*
     * The dashboard panels' border rather than a heavier one of its own: the
     * same 2px of dim green and the same small radius, on the same panel
     * background. The bar sits directly above the middle column with a rail of
     * panels either side of it, so anything else made it the loudest object on
     * a page it is not the subject of. panels.css also lines its top edge up
     * with the top of those rails.
     */
    border: 2px solid var(--accent-dim);
    border-radius: 4px;
    z-index: 2;
}

/*
 * Four across needs about 107px a button for the longest label at full size.
 * A phone held upright cannot give that to four columns and can to two, so the
 * base above is two by two and one row is the exception rather than the rule.
 */
#globe-control-buttons {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 8px;
}

@media (min-width: 600px) {
    #globe-control-buttons {
        grid-template-columns: repeat(4, 1fr);
    }
}

/*
 * Wider than high: the bar is a third of the page. A third of a 1600px screen
 * is 533px, which is comfortable for four; a third of a 1280px screen is 427px,
 * which is not at 14px type. The label size therefore tracks the viewport
 * between 9 and 14px so that four columns keep fitting all the way down, and
 * below about 1100px, where even 9px would not fit, the bar goes to two by two
 * instead and the type goes back up.
 */
@media (min-aspect-ratio: 1/1) {
    #globe-controls {
        width: 33.3333vw;
    }

    #globe-control-buttons {
        grid-template-columns: repeat(4, 1fr);
    }
}

@media (min-aspect-ratio: 1/1) and (max-width: 1100px) {
    #globe-control-buttons {
        grid-template-columns: repeat(2, 1fr);
    }
}

/*
 * The heading, and the way back to the resume inside it.
 *
 * A row of its own above the buttons rather than a fifth button: five columns
 * in a bar a third of the page wide leaves 97px each, and the longest label
 * needs 107px at this size, so the buttons would all have had to shrink to make
 * room for something that is not one of them.
 *
 * It is the page's only heading. The globe had none at all before this, which
 * left a document whose entire visible content was a canvas and a row of
 * controls, so an h1 here is worth having for its own sake as well as for
 * saying what the page is.
 *
 * Set quieter than the buttons under it despite being the heading. The controls
 * are what the bar is for; this says where you are and offers the way out, and
 * a heading loud enough to compete with four buttons would make the bar the
 * loudest object on a page it is not the subject of.
 */
#globe-controls-heading {
    margin: 0;
    padding: 1px 4px 0 4px;
    text-align: center;
    color: var(--text-secondary);
    font-family: var(--font-mono);
    font-size: 12px;
    font-weight: 400;
    letter-spacing: 0.08em;
    line-height: 1.3;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

#globe-home-link {
    color: var(--accent-secondary);
    text-decoration: none;
    border-bottom: 1px solid rgba(23, 201, 107, 0.4);
    transition: color 120ms ease, border-color 120ms ease;
}

#globe-home-link:focus-visible {
    outline: 2px solid var(--accent-secondary);
    outline-offset: 2px;
}

/*
 * Hover is asked for rather than assumed. See the note on the control buttons
 * below for why that matters on a touchscreen.
 */
@media (hover: hover) {
    #globe-home-link:hover {
        color: var(--text-primary);
        border-bottom-color: var(--text-primary);
    }
}

/*
 * The resume's control style: panel background, a solid green rule around it, a
 * small radius, monospace, letterspaced. The heavy visible border is the whole
 * look, so it is not softened here into the translucent hairline that a
 * floating overlay would normally get.
 */
.globe-control {
    appearance: none;
    -webkit-appearance: none;
    margin: 0;
    padding: 6px 14px;
    background-color: #0c1f12;
    border: 2px solid var(--border-subtle);
    border-radius: 4px;
    color: var(--accent-secondary);
    font-family: var(--font-mono);
    font-size: 14px;
    letter-spacing: 0.12em;
    line-height: 1.2;
    white-space: nowrap;
    cursor: pointer;
    transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
}

/*
 * The label size in the landscape bar, and the reason it is not a fixed number.
 * The bar is a third of the page there, so the space a button gets is decided
 * by the screen rather than by the label, and the longest of them - Lat / Lon -
 * needs about 6.5 times the font size in width plus its padding. Tracking the
 * viewport keeps four columns fitting from a 1600px screen, where there is room
 * for the full 14px, down to about 1100px, where the two by two rule below
 * takes over and the type can go back up.
 *
 * These two blocks sit after the base rule rather than beside their layout
 * partners above, because they set the same property at the same specificity
 * and the last one written is the one that applies.
 */
@media (min-aspect-ratio: 1/1) {
    .globe-control {
        padding: 6px 8px;
        font-size: clamp(9px, 0.85vw, 14px);
    }
}

@media (min-aspect-ratio: 1/1) and (max-width: 1100px) {
    .globe-control {
        font-size: 13px;
    }
}

/*
 * Hover, and the two things that were wrong with it.
 *
 * It is inside @media (hover: hover) so that it only applies to a pointer that
 * can actually hover. A touchscreen has no hover, but browsers emulate one
 * anyway: a tap leaves the :hover state stuck on the tapped element until
 * something else is tapped. On a page whose buttons are toggles that is not
 * cosmetic. Tapping Labels set aria-pressed to true, the sticky hover painted
 * over the pressed styling, and the button only turned green when the next
 * thing was tapped and the hover moved off it - so every toggle appeared to
 * report the state before last.
 *
 * The second problem was specificity, and it was there for a mouse too.
 * ".globe-control:hover:not(:disabled)" scores 0,3,0 against
 * ".globe-control[aria-pressed=\"true\"]" at 0,2,0, so hover won outright
 * wherever the two met. With a mouse that mostly went unnoticed because the
 * pointer moves away; the pressed rule below now also states its hover case, so
 * an on button hovered still reads as on.
 */
@media (hover: hover) {

    .globe-control:hover:not(:disabled) {
        background-color: var(--bg-elevated);
        border-color: var(--accent-primary);
        color: var(--text-primary);
    }

    /*
     * Brighter than the resting on state rather than different from it, so
     * hovering a lit button confirms the state instead of replacing it.
     */
    .globe-control[aria-pressed="true"]:hover:not(:disabled) {
        background-color: var(--accent-primary);
        border-color: var(--accent-secondary);
        color: var(--bg-panel);
        box-shadow: 0 0 10px rgba(0, 176, 96, 0.6);
    }
}

.globe-control:focus-visible {
    outline: 2px solid var(--accent-secondary);
    outline-offset: 2px;
}

/*
 * A toggle that is currently on. aria-pressed is the state, and the styling
 * hangs off it rather than off a separate class, so the two cannot disagree.
 *
 * On reads as a lit trace: the bright green promoted from the text to the
 * border and a glow behind it, rather than a filled block, which would fight
 * the panel it sits in.
 */
.globe-control[aria-pressed="true"] {
    background-color: var(--accent-dim);
    border-color: var(--accent-primary);
    color: var(--text-primary);
    box-shadow: 0 0 8px rgba(0, 176, 96, 0.45);
}

/*
 * The press itself, which a touchscreen otherwise gets nothing from.
 *
 * With hover gated off above, a tap on a phone had no visual acknowledgement at
 * all between the finger landing and the state changing. :active fires on the
 * touch itself and on both kinds of pointer, so it is the feedback that is
 * actually available everywhere.
 */
.globe-control:active:not(:disabled) {
    transform: translateY(1px);
}

/*
 * Nothing in the row is disabled any more, now that the panels button does
 * something. The rule stays for the next control that has to wait for the thing
 * it operates: unavailable should look unavailable rather than broken.
 */
.globe-control:disabled {
    opacity: 0.34;
    cursor: not-allowed;
}
