/* ------------------------------------------------------------------
   The chrome: the void it all floats in, the header, the footer panel.
   This layer is Voidality -- a projection, an instrument, a readout.
   ------------------------------------------------------------------ */

body {
  font-family: var(--font-ui);
  color: var(--text-0);
  background: var(--void-0);
  overflow-x: hidden;
}

/* ---------------------------------------------------------- the void */

.void {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(1100px 620px at 18% -8%,  rgba(37, 234, 181, 0.10), transparent 62%),
    radial-gradient(900px 520px at 88% 8%,    rgba(86, 120, 255, 0.075), transparent 60%),
    radial-gradient(1200px 800px at 50% 115%, rgba(37, 234, 181, 0.055), transparent 65%),
    linear-gradient(180deg, var(--void-1) 0%, var(--void-0) 55%, #020507 100%);
}

/* The projection's scanlines. Just visible enough to read as a hologram. */
.void::after {
  content: '';
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(
    180deg,
    rgba(37, 234, 181, 0.030) 0px,
    rgba(37, 234, 181, 0.030) 1px,
    transparent 1px,
    transparent 4px
  );
  mix-blend-mode: screen;
  opacity: .5;
}

@keyframes drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-60px, -40px, 0); }
}

.page {
  position: relative;
  z-index: 1;

  /* Held in all the time, not only while something is flying off to the side.
     A phone sizes its layout viewport around whatever overflows horizontally,
     so switching this on at the moment of a click changed the width of the
     window itself -- and the whole sky was re-fitted for the length of the
     journey and put back on arrival. */
  overflow-x: clip;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.wrap {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--rail);
}

/* ---------------------------------------------------------- header */

.topbar {
  position: sticky;
  top: 0;
  z-index: 40;
  backdrop-filter: blur(14px) saturate(130%);
  background: linear-gradient(180deg, rgba(4, 7, 10, 0.92), rgba(4, 7, 10, 0.68));
  border-bottom: 1px solid var(--line);
}

/* How tall the bar is, written down once so anything that has to sit under
   it -- the hub's drawer, for one -- can be told where "under it" is. */
:root { --barh: 4rem; }
@media (max-width: 640px) { :root { --barh: 3.5rem; } }

.topbar__in {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  min-height: var(--barh);
}

.brand { display: flex; align-items: center; gap: .7rem; flex: 0 0 auto; }
.brand img { width: 2rem; height: 2rem; filter: drop-shadow(0 0 8px var(--accent-glow)); }
.brand__name {
  font-family: var(--font-hud);
  font-size: .82rem;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--text-0);
}
.brand__name b { color: var(--accent); font-weight: 500; }

.nav { display: flex; align-items: center; gap: .25rem; margin-left: auto; }

.nav a, .nav button.navlink {
  position: relative;
  padding: .5rem .8rem;
  font-family: var(--font-hud);
  font-size: .72rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--text-1);
  border-radius: 3px;
  transition: color var(--t-state) var(--ease-out), background var(--t-state) var(--ease-out);
}
.nav a:hover, .nav button.navlink:hover { color: var(--accent); background: var(--accent-faint); }
.nav a[aria-current="page"] { color: var(--accent); }
.nav a[aria-current="page"]::after {
  content: '';
  position: absolute;
  left: .8rem; right: .8rem; bottom: .25rem;
  height: 1px;
  background: var(--accent);
  box-shadow: 0 0 8px var(--accent-glow);
}

.nav__cta {
  margin-left: .5rem;
  padding: .45rem .9rem !important;
  color: var(--void-0) !important;
  background: var(--accent) !important;
  font-weight: 600;
  border-radius: 3px;
}
.nav__cta:hover { background: var(--accent-pale) !important; }

/* The log, up but not public: the word with a line drawn through it.
   It used to read "The Log · hidden", which is a label explaining a state
   instead of the state being visible. A struck word is read at a glance and
   needs no vocabulary. The word is still in the markup for a screen reader,
   followed by ", hidden", because a line is a picture and says nothing aloud. */
.nav .struck { position: relative; }
.nav .struck::after {
  content: '';
  position: absolute;
  left: -.12em;
  right: -.12em;
  top: 52%;
  height: 1px;
  background: currentColor;
  box-shadow: 0 0 6px var(--accent-glow);
  opacity: .85;
  pointer-events: none;
}

/* Said, never shown. */
.u-sr {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------- the burger */
/*
   Hidden until the places stop fitting across the bar. Three rules rather
   than a glyph, so it can become a cross without swapping any characters --
   the top and bottom rules rotate onto each other and the middle one goes.
*/

.burger {
  display: none;
  position: relative;
  margin-left: auto;
  width: 2.5rem;
  height: 2.5rem;
  flex: 0 0 auto;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: transparent;
  cursor: pointer;
  transition: border-color var(--t-state) var(--ease-out),
              background var(--t-state) var(--ease-out);
}
.burger:hover { border-color: var(--accent); background: var(--accent-faint); }

.burger span {
  position: absolute;
  left: 50%;
  width: 1.1rem;
  height: 1px;
  margin-left: -.55rem;
  background: var(--accent);
  box-shadow: 0 0 6px var(--accent-glow);
  transition: transform var(--t-state) var(--ease-out),
              opacity var(--t-state) var(--ease-out);
}
.burger span:nth-child(1) { top: .85rem; }
.burger span:nth-child(2) { top: 1.22rem; }
.burger span:nth-child(3) { top: 1.59rem; }

.burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(.37rem) rotate(45deg); }
.burger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.burger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-.37rem) rotate(-45deg); }

/* ---------------------------------------------------------- footer panel */
/* The instrument panel. It counts things that do not matter, with
   complete sincerity, which is the only correct way to count them. */

.panel {
  margin-top: auto;
  border-top: 1px solid var(--line);
  background: linear-gradient(180deg, rgba(7, 12, 18, 0.5), rgba(2, 5, 7, 0.92));
}

.panel__in { padding-block: 2.25rem 1.5rem; }

.readouts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: .75rem 1.5rem;
  padding-bottom: 1.5rem;
  border-bottom: 1px solid var(--line);
}

.readout {
  display: flex;
  flex-direction: column;
  gap: .2rem;
  font-family: var(--font-hud);
}
.readout__k {
  font-size: .6rem;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--text-2);
}
.readout__v {
  font-size: 1rem;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 14px var(--accent-glow);
}

.panel__feet {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-top: 1.25rem;
  font-size: .78rem;
  color: var(--text-2);
}
.panel__feet a { color: var(--text-1); transition: color var(--t-state) var(--ease-out); }
.panel__feet a:hover { color: var(--accent); }
.panel__feet nav { display: flex; gap: 1.1rem; flex-wrap: wrap; }

/* ---------------------------------------------------------- shared bits */

.hud-label {
  font-family: var(--font-hud);
  font-size: .64rem;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: var(--accent);
}

.rule {
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--line) 15%, var(--line) 85%, transparent);
  border: 0;
}

/* Bracket corners. The projection's frame, used wherever a panel needs edges. */
.bracket { position: relative; }
.bracket::before, .bracket::after {
  content: '';
  position: absolute;
  width: 14px; height: 14px;
  border: 1px solid var(--accent-deep);
  opacity: .6;
  pointer-events: none;
}
.bracket::before { top: -1px; left: -1px; border-right: 0; border-bottom: 0; }
.bracket::after  { bottom: -1px; right: -1px; border-left: 0; border-top: 0; }

/* Notices. They drop in from the top of the window, stack, and fold away
   (see say() in core.js). After the old site's alerts -- the slide down
   with a small overshoot, a title over a line -- in this site's dress:
   dark glass, a lit edge in the notice's colour, the HUD face for the title,
   and a fuse along the foot that burns down as it waits. */
.notices {
  position: fixed;
  z-index: 200;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  width: min(28rem, calc(100vw - 2rem));
  padding-top: .85rem;
  pointer-events: none;
}
.notice {
  --tone: var(--accent);
  --tone-faint: var(--accent-faint);
  position: relative;
  overflow: hidden;
  pointer-events: auto;
  cursor: pointer;
  margin-bottom: .55rem;
  padding: .7rem 1rem .8rem;
  max-height: 14rem;
  background:
    linear-gradient(180deg, var(--tone-faint), transparent 70%),
    rgba(7, 12, 18, 0.95);
  border: 1px solid var(--line);
  border-top: 2px solid var(--tone);
  border-radius: 3px;
  box-shadow: 0 14px 40px rgba(0, 0, 0, 0.55), 0 0 22px -8px var(--tone);
  backdrop-filter: blur(6px);
  opacity: 0;
  transform: translateY(-140%);
  transition:
    transform 420ms var(--ease-snap),
    opacity 220ms var(--ease-out),
    max-height 300ms var(--ease-both) 120ms,
    margin 300ms var(--ease-both) 120ms,
    padding 300ms var(--ease-both) 120ms;
}
.notice.is-in  { opacity: 1; transform: none; }
.notice.is-pop { animation: noticePop 260ms var(--ease-snap); }
.notice.is-out {
  opacity: 0;
  transform: translateY(-60%);
  max-height: 0 !important;
  margin-bottom: 0;
  padding-top: 0;
  padding-bottom: 0;
  transition:
    transform 240ms var(--ease-in),
    opacity 200ms var(--ease-in),
    max-height 280ms var(--ease-both) 100ms,
    margin 280ms var(--ease-both) 100ms,
    padding 280ms var(--ease-both) 100ms;
}
.notice--bad  { --tone: var(--danger); --tone-faint: rgba(255, 107, 94, 0.08); }
.notice--warn { --tone: var(--warn);   --tone-faint: rgba(240, 192, 96, 0.08); }

.notice__title {
  font-family: var(--font-hud);
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--tone);
  margin-bottom: .3rem;
}
.notice__text {
  font-family: var(--font-ui);
  font-size: .9rem;
  line-height: 1.45;
  color: var(--text-0);
}
.notice__fuse {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 2px;
  background: var(--tone);
  opacity: .55;
  transform-origin: left center;
}
@keyframes noticePop { 40% { transform: scale(1.025); } }

@media (prefers-reduced-motion: reduce) {
  .notice, .notice.is-out { transform: none; }
}

/* ---------------------------------------------------------- the bar, folded */
/*
   Six places and a sign-out do not fit across a phone, and shrinking them
   until they do leaves a row of unreadable stubs. Below this width the SAME
   nav -- not a second copy of it -- becomes a panel under the bar. One menu,
   one order, one set of position numbers.

   It hangs off .topbar rather than off .topbar__in, so it spans the whole
   width instead of stopping at the rails the content is held inside.
*/
@media (max-width: 820px) {
  .burger { display: block; }

  .nav {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    margin-left: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: .4rem;
    background: linear-gradient(180deg, rgba(4, 7, 10, .98), rgba(4, 7, 10, .95));
    border-bottom: 1px solid var(--line);
    backdrop-filter: blur(14px) saturate(130%);
    box-shadow: 0 18px 40px rgba(0, 0, 0, .55);

    /* Folded away rather than taken out of the page, so opening it is a
       movement and the links keep their place in the reading order. */
    transform-origin: top center;
    transform: scaleY(.55) translateY(-.4rem);
    opacity: 0;
    visibility: hidden;
    transition:
      transform var(--t-state) var(--ease-out),
      opacity var(--t-state) var(--ease-out),
      visibility 0s linear var(--t-state);
  }
  .nav.is-open {
    transform: none;
    opacity: 1;
    visibility: visible;
    transition-delay: 0s, 0s, 0s;
  }

  .nav a, .nav button.navlink {
    padding: .85rem 1rem;
    font-size: .74rem;
    letter-spacing: .14em;
    text-align: left;
  }
  /* The mark for "you are here" is an underline along a row. Down a list it
     has to be a left edge, or it reads as a strikethrough. */
  .nav a[aria-current="page"]::after {
    left: 0;
    right: auto;
    top: .5rem;
    bottom: .5rem;
    width: 1px;
    height: auto;
  }
  .nav__cta { margin-left: 0; margin-top: .35rem; text-align: center !important; }
}

@media (max-width: 640px) {
  .topbar__in { gap: .6rem; }
  .brand__name { font-size: .7rem; letter-spacing: .16em; }

  /* Two across. The panel's columns are normally sized by content, and on a
     phone nothing fit beside anything else, so eight small jokes became eight
     screens of scrolling. Fixed at two, with a long name wrapping inside its
     own half rather than pushing for a width that is not there. */
  .readouts {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1.05rem .9rem;
  }
  .readout__k {
    font-size: .55rem;
    letter-spacing: .13em;
    line-height: 1.35;
    overflow-wrap: anywhere;
  }
  .readout__v { font-size: .95rem; }

  .panel__feet { justify-content: flex-start; gap: .7rem 1rem; }
}

/* ---------------------------------------------------------- figures in the stars */
/*
   Faint on purpose. The lines are barely drawn and the stars in them are only
   a shade brighter than the dust, so the shapes resolve for somebody who looks
   twice and stay a star field for everybody else. They drift with the dust, at
   the same rate, so the sky moves as one thing.
*/
/* The whole sky: the dust, the haze of the galaxy, and the one figure in it.

   Sized by inset alone and by nothing else, so the drawing stays centred on
   the window. Setting a width as well left it anchored at the left edge and
   a tenth of the window narrower than it looked, which put everything in it
   a hundred and fifty pixels left of where it was meant to be. */
.figures {
  position: absolute;
  left: -8%;
  top: -8vh;
  right: auto;
  bottom: auto;
  width: 116%;

  /* Height in vh rather than per cent of the backdrop. On a phone the
     backdrop grows and shrinks as the address bar comes and goes, and a
     per-cent height meant the whole sky was re-fitted every time -- which
     is every page change -- and every star landed somewhere else. vh is
     pinned to the tall viewport and does not move. */
  height: 116vh;

  /* The reset caps every svg at `max-width: 100%` and makes its height
     follow its own proportions. Both have to be lifted here or this one
     stays exactly the width of the window while being offset left of it,
     which is why whatever was drawn in it kept landing off centre. */
  max-width: none;
  max-height: none;

  filter: drop-shadow(0 0 2px rgba(255,255,255,.35));
  animation: drift 300s ease-in-out infinite alternate;

  /* Started as far through itself as the sky clock says, so a page change
     does not send every star home. */
  animation-delay: var(--drift-at, 0s);
}

/* Every star, placed at random rather than tiled. A tile repeats and the eye
   finds the grid in it at once. */
/* The figure does not drift with the dust: it has to stay centred. */
.figures--still { animation: none; }

.dust circle { fill: rgba(255, 255, 255, .62); }
.dust circle:nth-child(3n)   { fill: rgba(190, 235, 255, .55); }
.dust circle:nth-child(7n)   { fill: rgba(157, 246, 222, .5); }
.dust circle:nth-child(11n)  { fill: rgba(255, 255, 255, .85); }
.dust circle:nth-child(13n)  { fill: rgba(255, 255, 255, .34); }

/* The twinkle. Opacity only -- nothing here moves, so a page change cannot
   jolt the sky the way the drift once did. Each star carries its own period,
   its own phase and its own floor from the generator, so the field never
   pulses in step, and a star resuming mid-cycle after a page change looks
   exactly like a star that never stopped. */
/* Every star breathes. The ones big enough for it to register also swell --
   through a transform rather than through `r`, because animating SVG geometry
   on seven hundred elements invalidates the whole drawing every frame and
   measured four times the cost of a transform doing the same thing. */
@keyframes twinkle {
  0%, 100% { opacity: 1; }
  11%      { opacity: var(--tl, .5); }
  19%      { opacity: .88; }
  27%      { opacity: calc(var(--tl, .5) * 1.06); }
  38%      { opacity: 1; }
  47%      { opacity: calc(var(--tl, .5) * 1.03); }
  58%      { opacity: .95; }
  69%      { opacity: var(--tl, .5); }
  80%      { opacity: .9; }
  91%      { opacity: calc(var(--tl, .5) * 1.1); }
}
@keyframes twinkleSwell {
  0%, 100% { opacity: 1;                               transform: scale(var(--sw, 1.15)); }
  11%      { opacity: var(--tl, .5);                   transform: scale(.93); }
  19%      { opacity: .88;                             transform: scale(calc(var(--sw, 1.15) * .92)); }
  27%      { opacity: calc(var(--tl, .5) * 1.06);      transform: scale(.95); }
  38%      { opacity: 1;                               transform: scale(var(--sw, 1.15)); }
  47%      { opacity: calc(var(--tl, .5) * 1.03);      transform: scale(.95); }
  58%      { opacity: .95;                             transform: scale(calc(var(--sw, 1.15) * .96)); }
  69%      { opacity: var(--tl, .5);                   transform: scale(.94); }
  80%      { opacity: .9;                              transform: scale(calc(var(--sw, 1.15) * .9)); }
  91%      { opacity: calc(var(--tl, .5) * 1.1);       transform: scale(.97); }
}
.tw {
  animation: twinkle var(--tw, 1.6s) linear var(--td, 0s) infinite;
}
.tw--swell {
  transform-box: fill-box;
  transform-origin: 50% 50%;
  animation-name: twinkleSwell;
}

.fig__l {
  fill: none;
  stroke: rgba(157, 246, 222, .055);
  stroke-width: 1;
  stroke-linejoin: round;
}
.fig__s      { fill: rgba(255, 255, 255, .8); }
.fig__s--pale { fill: rgba(190, 235, 255, .72); }

/* ---------------------------------------------------------- travelling */
/*
   The backdrop is space, so one place does not simply replace another: the one
   being left falls away and the one being arrived at comes up to meet it, with
   the stars pushing past in between. Short, because a transition that has to be
   waited through is a transition in the way.
*/
/*
   Only the contents travel. The banner and the panel stay where they are,
   because they are the same banner and the same panel on both sides of the
   journey -- it is what is between them that changes.

   Which way it goes is where the two places sit beside each other: going on
   down the list, what you are leaving flies off to the left and shrinks into
   the distance and what you are going to comes up from the right; going back,
   the other way about. --travel-x carries the direction, set before the
   animation starts.
*/
body.is-leaving  > .page > main { animation: contentAway var(--t-fall) var(--ease-in) both; }
body.is-arriving > .page > main { animation: contentHere var(--t-rise) var(--ease-out) both; }

/* Shrink away toward a point near the top of the window, not toward the
   middle of the page.

   The default origin is the middle of the element, which is fine while every
   page is about a screen tall and nonsense the moment one is not. The Stock
   runs four thousand pixels deep, so its middle sits two thousand pixels below
   the window and the whole thing collapsed toward a point nobody could see --
   which read as a different animation entirely, and as coming from below
   whichever way it had actually gone. */
body.is-leaving  > .page > main,
body.is-arriving > .page > main { transform-origin: 50% min(45vh, 50%); }



/* The sky itself does not move for this. It used to be pushed out a little
   on the way out and brought back on the way in, which meant a jump at the
   navigation between the end of one and the start of the other -- the stars
   zooming and then snapping home. Only the contents travel. */

@keyframes contentAway {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateX(var(--travel-x, -46vw)) scale(.58); }
}
@keyframes contentHere {
  from { opacity: 0; transform: translateX(var(--travel-x, 46vw)) scale(.58); }
  to   { opacity: 1; transform: none; }
}

/* Arrived without having travelled -- a typed address, a reload, the redirect
   after saving something. There is no side it came from, so it does not come
   from one. */
body.is-arriving[data-way="still"] > .page > main { animation-name: contentStill; }

@keyframes contentStill {
  from { opacity: 0; transform: scale(.985); }
  to   { opacity: 1; transform: none; }
}

/* The hub's places are a list down the side rather than a row along the top,
   so its contents come and go the way its menu reads: up and down. */
body.is-leaving[data-way="down"]  > .page > main { animation-name: contentAwayDown; }
body.is-arriving[data-way="down"] > .page > main { animation-name: contentHereDown; }

@keyframes contentAwayDown {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateY(var(--travel-y, -40vh)) scale(.58); }
}
@keyframes contentHereDown {
  from { opacity: 0; transform: translateY(var(--travel-y, 40vh)) scale(.58); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  body.is-arriving > .page > main, body.is-leaving > .page > main { animation: none; }
  .figures { animation: none; }
  .tw, .tw--swell { animation: none; }
}


/* ------------------------------------------------------- the logo show
   Clicking the header mark plays the logo assembling itself (js/logoShow.js).
   The veil dims the page but never blacks it out. */
.brand, .brand img { cursor: pointer; }
.logoshow {
  position: fixed;
  inset: 0;
  z-index: 300;
  display: grid;
  place-items: center;
  background: rgba(2, 6, 8, .78);
  cursor: pointer;
  opacity: 0;
}
.logoshow__stage { width: min(78vmin, 560px); aspect-ratio: 1; }
.logoshow__stage svg { display: block; width: 100%; height: 100%; overflow: visible; }
.logoshow__lockup { display: flex; align-items: center; }
.logoshow__stage { flex: 0 0 auto; }
.logoshow__name { width: 0; overflow: hidden; display: flex; justify-content: flex-end; flex: 0 0 auto; }
.logoshow__name .brand__name { white-space: nowrap; flex: 0 0 auto; }

/* ------------------------------------------------------------------
   The bell.

   Site news, left of the places on a desktop and left of the burger on a
   phone. It carries no `data-pos` and gets no travel animation, because it
   is not a place you go to -- it opens where it stands.
   ------------------------------------------------------------------ */

/* ⚠️ The auto margin moves from .nav to the bell when a bell is there, rather
   than both of them having one -- two auto margins split the free space
   between them, which would strand the bell in the middle of the bar. The
   sibling selectors leave the signed-out header, which has no bell, exactly
   as it was. */
/* ⚠⚠ NO BORDER ON THE BELL, EVER. It had `border: 1px solid transparent`
   turning solid on hover and while open -- and it sits directly beside the
   places, so a cursor on its way to a tab crosses it. Changing page with the
   pointer resting there drew the box, took it away while the next page
   arrived, and drew it again: *"a box blinks around the notification bell
   when I change pages."* A thing with no border cannot blink one. The tint
   and the colour say everything the box was saying. */
.bell {
  position: relative;
  display: grid;
  place-items: center;
  margin-left: auto;
  width: 2.3rem;
  height: 2.3rem;
  flex: 0 0 auto;
  padding: 0;
  border: 0;
  border-radius: 3px;
  background: transparent;
  color: var(--text-1);
  cursor: pointer;
  transition: color var(--t-state) var(--ease-out),
              background var(--t-state) var(--ease-out);
}
.bell ~ .nav,
.bell ~ .burger { margin-left: 0; }

.bell svg {
  width: 1.15rem;
  height: 1.15rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.bell:hover, .bell[aria-expanded="true"] {
  color: var(--accent);
  background: var(--accent-faint);
}
.bell:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The count. It is printed by PHP, so the number is right in the first frame
   rather than appearing a moment after the page does. */
.bell__n {
  position: absolute;
  top: -.15rem;
  right: -.2rem;
  min-width: 1.05rem;
  height: 1.05rem;
  padding: 0 .2rem;
  border-radius: 999px;
  background: var(--accent);
  color: var(--void-0);
  font-family: var(--font-hud);
  font-size: .55rem;
  font-weight: 600;
  line-height: 1.05rem;
  text-align: center;
  box-shadow: 0 0 10px var(--accent-glow);
}
.bell.is-ringing svg { color: var(--accent); }

/* Fixed under the bar rather than absolute inside it: .topbar__in is the
   content rail and the panel wants the edge of the glass, and on a phone the
   nav panel is already using the bar as its containing block. */
.bellpanel {
  position: fixed;
  top: calc(var(--barh) + .3rem);
  right: var(--rail);
  z-index: 80;
  width: min(22rem, calc(100vw - 1.6rem));
  max-height: min(26rem, calc(100vh - var(--barh) - 2rem));
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: rgba(7, 12, 18, .97);
  backdrop-filter: blur(8px);
  box-shadow: 0 14px 40px rgba(0, 0, 0, .66);
}
.bellpanel[hidden] { display: none; }

.bellpanel__head {
  position: sticky;
  top: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  padding: .6rem .75rem;
  border-bottom: 1px solid var(--line);
  background: rgba(7, 12, 18, .97);
  font-family: var(--font-hud);
  font-size: .6rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--label);
}
.bellpanel__all {
  border: 0;
  background: none;
  color: var(--text-2);
  font-family: inherit;
  font-size: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  cursor: pointer;
}
.bellpanel__all:hover { color: var(--accent); }

.bellpanel__list { list-style: none; margin: 0; padding: .3rem; }
.bellpanel__bare {
  padding: .9rem .75rem;
  font-size: .82rem;
  line-height: 1.5;
  color: var(--text-2);
}

.bellitem { border-radius: 4px; }
.bellitem + .bellitem { margin-top: .15rem; }
.bellitem__in {
  display: grid;
  grid-template-columns: auto 1fr auto;
  grid-template-areas: "k t w" ". s s";
  gap: .15rem .5rem;
  padding: .5rem .6rem;
  text-decoration: none;
  color: inherit;
}
a.bellitem__in:hover { background: var(--accent-faint); border-radius: 4px; }

.bellitem__k {
  grid-area: k;
  align-self: center;
  font-family: var(--font-hud);
  font-size: .5rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--text-2);
}
.bellitem__t { grid-area: t; font-size: .86rem; color: var(--text-1); }
.bellitem__w {
  grid-area: w;
  align-self: center;
  font-family: var(--font-hud);
  font-size: .52rem;
  color: var(--text-2);
  white-space: nowrap;
}
.bellitem__say { grid-area: s; font-size: .78rem; line-height: 1.45; color: var(--text-2); }

/* Unread. A dot in the margin rather than a block of colour: a panel of six
   notices, every one of them highlighted, highlights nothing. */
.bellitem.is-new .bellitem__t { color: var(--text-0); font-weight: 500; }
.bellitem.is-new .bellitem__k { color: var(--accent); }
.bellitem.is-new { box-shadow: inset 2px 0 0 var(--accent); }

@media (max-width: 640px) {
  .bellpanel { right: .6rem; }
  .bell { width: 2.1rem; height: 2.1rem; }
}

/* ------------------------------------------------------------------
   TRAVELLING, SECOND PASS -- making it reliable on a real host.
   (Seth, 2026-10-28: *"the pages get really jumpy there where they blink
   mid transition. Is there a way we can make that reliable even on the
   hosted site?"*)

   ⚠️⚠️ THE BLINK WAS NOT A STYLING FAULT. The old sequence was: play the
   exit animation, THEN navigate. Measured on a link throttled to 180ms of
   latency, `main` sat at opacity 0 for 198ms after the exit finished and
   before anything arrived -- an empty page with only the sky and the bar on
   it. Locally the fetch is ~10ms so it never showed; on a real host it is
   the whole of the complaint.

   Two fixes, and the first is the one that makes it RELIABLE rather than
   usually-fine:

   1. A cross-document view transition. The browser holds the old frame
      itself until the new document is ready to paint, so a gap is not
      possible -- there is nothing to hold wrong, no timer to get wrong and
      no guess about how slow the host is. Chrome, Edge and Safari 18 do
      this; anything else ignores the at-rule entirely.
   2. For everything else: the exit animation now plays DURING the load
      rather than before it (core.js no longer defers the navigation), so
      the dead window is gone there too.
   ------------------------------------------------------------------ */

@view-transition { navigation: auto; }

/* Only the contents travel, exactly as before. Lifting main out of the root
   snapshot leaves the sky, the bar and the player to cross-fade between two
   frames that are identical, which is invisible. */
.page > main { view-transition-name: content; }

/* ⚠️ Both halves animate in ONE document now, so they need two directions
   rather than the single `--travel-x` the two-document version flipped
   between. `data-came` is on <html> before the first paint -- see the inline
   script in includes/head.php -- and says which way this page was reached. */
:root {
  --vt-out: -46vw; --vt-in: 46vw;
  --vt-outY: 0; --vt-inY: 0;
}
html[data-came="back"]  { --vt-out:  46vw; --vt-in: -46vw; }
html[data-came="don"]   { --vt-out: 0; --vt-in: 0; --vt-outY: -40vh; --vt-inY:  40vh; }
html[data-came="dback"] { --vt-out: 0; --vt-in: 0; --vt-outY:  40vh; --vt-inY: -40vh; }

/* Arrived without having travelled: a typed address, a reload, the redirect
   after saving. No side to come from, so it does not come from one. */
html[data-came="still"] { --vt-out: 0; --vt-in: 0; --vt-outY: 0; --vt-inY: 0; }

/* ⭐⭐ OUT, THEN IN -- NOT BOTH AT ONCE.
   A view transition runs the two halves concurrently by default, so the page
   leaving and the page arriving were on the glass together, each half way
   through its own movement. That is the "jumpy": two things moving over each
   other with nothing to tell them apart.

   The Score's panels do it sequentially -- the old one goes, and only then
   does the new one come -- and that is the one he said was right. A delay on
   the arriving half the length of the leaving half is the whole difference,
   and it makes a cross-document journey feel like the in-page one. */
::view-transition-old(content) {
  animation: vtAway var(--t-fall) var(--ease-in) both;
}
::view-transition-new(content) {
  animation: vtHere var(--t-rise) var(--ease-out) both;
  animation-delay: var(--t-fall);
}
/* The group is what morphs the box from its old size to its new one. Left at
   its own default it finished long before the halves inside it did, and the
   contents snapped to the new size mid-flight. */
::view-transition-group(content) {
  animation-duration: calc(var(--t-fall) + var(--t-rise));
}

/* ⭐⭐ WHAT TRAVELS IS A SCREENFUL, NOT THE WHOLE PAGE.
   (Seth, 2026-10-29: *"namely the stock tab.... it doesn't animate like it
   should."*)

   A view transition snapshots the NAMED ELEMENT, at its full size. Measured:
   the hub's Stock page is a 7490px column, against about 880px for Accounts
   and The Books, in a 1000px window. So the Stock tab was sliding a snapshot
   seven screens tall and morphing it down to one — which is a completely
   different movement from the one every other tab makes, and exactly what he
   saw.

   These two lines make the snapshot behave as a screenful taken from the top
   rather than as a tall picture scaled to fit: `object-fit: none` stops it
   being squashed into the group's box, and the cap stops a long page
   contributing more of itself than anybody could see anyway.

   ⚠️ It applies to every page, not only the hub — the public pages differ
   in height too, and a journey that looks different because the page you
   left happened to be long is the same fault wearing other clothes. */
::view-transition-old(content),
::view-transition-new(content) {
  max-height: 100vh;
  object-fit: none;
  object-position: center top;
  overflow: clip;
}

/* ⭐⭐ INSIDE THE HUB, THE MENU HOLDS STILL AND ONLY THE WORK TRAVELS.
   (Seth, 2026-10-29: *"the menu is gliding with the pages now..... it used
   to stay put."*)

   It used to be held by an override in admin.css on `body.is-leaving` -- and
   the view transition does not use those classes, so the override stopped
   applying and `main`, menu and all, became the thing that travels.

   Naming is how you say "this does not travel" to a view transition. The
   menu is named and given a zero-length group, and the travelling name moves
   off `main` and onto `.hubbody`, which is the column the hub was always
   animating on its own. */
body[data-page="hub"] .page > main { view-transition-name: none; }
body[data-page="hub"] .hubnav  { view-transition-name: hubmenu; }
body[data-page="hub"] .hubbody { view-transition-name: content; }

::view-transition-group(hubmenu) { animation-duration: 0s; }
::view-transition-old(hubmenu),
::view-transition-new(hubmenu) { animation: none; }
@keyframes vtAway {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translate(var(--vt-out), var(--vt-outY)) scale(.58); }
}
@keyframes vtHere {
  from { opacity: 0; transform: translate(var(--vt-in), var(--vt-inY)) scale(.58); }
  to   { opacity: 1; transform: none; }
}

/* ⚠⚠ THE SKY AND THE BAR ARE HELD BY NAMING THEM, NOT BY SWITCHING THE
   ROOT SNAPSHOT OFF.

   The first version of this killed `::view-transition-old(root)` outright,
   reasoning that two identical headers cross-fading is a waste of a frame.
   Caught in a frame capture: it made the header go BLANK for part of the
   transition, which is a worse flicker than the one it was avoiding. The
   root snapshot is not just the bar -- it is everything that is not named,
   and removing it removes all of that.

   Naming them is the supported way to say "this does not travel": the
   browser morphs each one from its old position to its new, and since they
   are identical on both pages the morph has nothing to do. */
.topbar { view-transition-name: bar; }
.void   { view-transition-name: sky; }

::view-transition-group(bar),
::view-transition-group(sky) { animation-duration: 0s; }
::view-transition-old(bar), ::view-transition-new(bar),
::view-transition-old(sky), ::view-transition-new(sky) { animation: none; }

/* The player is outside `.page` and must not be snapshotted into the
   transition at all -- it is the one thing on screen that is genuinely live
   while the page changes. */
.score { view-transition-name: player; }
::view-transition-group(player) { animation-duration: 0s; }
::view-transition-old(player), ::view-transition-new(player) { animation: none; }

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(content),
  ::view-transition-new(content) { animation: none; }
}

/* hello@ beside the copyright line (includes/foot.php). */
.panel__hello { margin-left: .9rem; color: var(--text-2); }
.panel__hello a { color: var(--accent); }
@media (max-width: 640px) { .panel__hello { display: block; margin: .35rem 0 0; } }
