/* Shantell Sans, self-hosted rather than pulled from Google's CDN: no external
   request on load, nothing third-party watching who visits, and the font cannot
   disappear out from under the page. Variable weight 300-800 in one file per
   subset. Licensed under the SIL Open Font License; see fonts/OFL.txt. */
@font-face {
  font-family: 'Shantell Sans';
  font-style: normal;
  font-weight: 300 800;
  font-display: swap;
  src: url('/fonts/shantell-sans-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+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Shantell Sans';
  font-style: normal;
  font-weight: 300 800;
  font-display: swap;
  src: url('/fonts/shantell-sans-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  /* The family palette, a step darker than upload and manga on purpose: the
     cat's body is drawn in exactly #2b2a33, so on the lighter ground he had
     1.00:1 contrast and only his ears and whiskers showed. */
  --bg: #1c1b22;
  --card-2: #3d3b49;
  --sunk: #16151c;
  --line: #474553;
  --text: #f2eff7;
  --dim: #a49fb2;
  --lav-light: #d2ace8;
  /* The recurring set, all taken from the drawings: the ink the cat and bubble
     are outlined in, the lavender of his ears, and two deeper steps of it that
     carry every button and link. */
  --line-ink: #2b2a33;
  --lav: #ab80d1;          /* the cat's ears; the anchor of the whole set */
  --lav-deep: #8a55b8;
  --lav-deeper: #7a47a8;
  --bad: #ff6c6c;
  --good: #8fdba8;
}
* { box-sizing: border-box; }
/* An explicit `display` in author CSS beats the [hidden] attribute, so a
   `<label hidden>` stayed visible because of `label { display: block }`. Make
   hidden win everywhere rather than patching it per element. */
[hidden] { display: none !important; }
body {
  margin: 0; min-height: 100vh;
  background: var(--bg); color: var(--text);
  font: 16px/1.55 'Shantell Sans', ui-rounded, ui-sans-serif, system-ui, sans-serif;
  padding: 1.5rem;
}

/* The centring lives on a wrapper, not on body.
   Password managers and other extensions append their notification bars
   straight to <body>. With body as the centring grid, that bar became a second
   grid item, the grid grew a row, and everything shifted up to make room for it
   the moment you signed in. Anything injected now lands beside this wrapper
   instead of inside the layout. */
.page {
  min-height: calc(100vh - 3rem);
  display: grid;
  place-items: center;
}
/* No panel behind the content. A rounded card floating on a dark background is
   the single most template-looking thing a login page can do, and it fenced the
   cat into a box. The inputs carry their own surfaces, so the form still reads
   as a form without one. */
.card {
  width: 100%; max-width: 1320px;
}
/* The account panel now lives inside the bubble, so it takes the bubble's
   width rather than setting its own. */
#account { max-width: none; margin: 0; }

/* The admin board is the one thing too big for the bubble. It sits underneath
   on the dark page, at its own width, and you scroll to it. */
#admin {
  max-width: 900px;
  margin: 2.5rem auto 0;
}
h2 { font-size: 1rem; font-weight: 600; margin: 0 0 1rem; }
.sub { margin: 0 0 1.5rem; color: var(--dim); font-size: .9rem; }
label { display: block; margin-bottom: 1rem; font-size: .82rem; color: var(--dim); }
input {
  width: 100%; margin-top: .35rem; padding: .6rem .7rem;
  background: var(--sunk); color: var(--text);
  border: 1px solid var(--line); border-radius: 8px; font-size: .95rem;
}
input:focus { outline: none; border-color: var(--lav); }
button {
  width: 100%; padding: .65rem; border: 0; border-radius: 8px;
  background: var(--lav-deep); color: #fff; font-size: .95rem; font-weight: 600;
  cursor: pointer;
}
button:hover { background: var(--lav-deeper); color: #fff; }
button:disabled { opacity: .6; cursor: default; }
button.secondary:hover { background: transparent; border-color: var(--lav); color: var(--text); }
button.secondary { background: transparent; border: 1px solid var(--line); color: var(--dim); margin-top: .75rem; }
.msg { margin: 1rem 0 0; font-size: .85rem; color: var(--bad); }
.msg.ok { color: var(--good); }
.toggle { margin: 1.25rem 0 0; font-size: .82rem; color: var(--dim); text-align: center; }
.toggle a { color: var(--lav-light); text-decoration: none; }

/* ---- admin board ---- */
.hint { color: var(--dim); font-size: .8rem; margin: 0 0 1rem; }
.table-scroll { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: .85rem; }
th, td { padding: .5rem .6rem; text-align: left; border-bottom: 1px solid var(--line); white-space: nowrap; }
th { color: var(--dim); font-weight: 600; font-size: .75rem; text-transform: uppercase; letter-spacing: .04em; }
td.user { font-weight: 600; }
td.dim { color: var(--dim); font-weight: 400; }
.tag { font-size: .7rem; padding: .1rem .4rem; border-radius: 4px; background: var(--lav-deep); color: #fff; margin-left: .4rem; }
.danger {
  background: var(--bad); border: 1px solid var(--bad); color: #2a1114;
  cursor: pointer; font-size: .72rem; font-weight: 600; width: auto;
  padding: .24rem .6rem; border-radius: 6px; white-space: nowrap;
}
.danger:hover { filter: brightness(1.12); }
.danger:active { filter: brightness(0.94); }

h2.spaced { margin-top: 1.4rem; }

select {
  background: var(--sunk); color: var(--text); border: 1px solid var(--line);
  border-radius: 6px; padding: .3rem .4rem; font-size: .8rem;
}

/* ---- access chips: dark when off, lit when on ---- */
.chips { display: flex; flex-wrap: wrap; gap: .3rem; }
.chip {
  width: auto; background: var(--card-2); border: 1px solid var(--line); color: var(--dim);
  padding: .22rem .6rem; border-radius: 999px; font-size: .72rem; font-weight: 600;
  cursor: pointer; transition: background .12s, color .12s, border-color .12s;
}
.chip:hover { border-color: var(--lav); color: var(--text); filter: none; }
.chip.on {
  background: var(--lav-deep); border-color: var(--lav-deep); color: #fff;
  box-shadow: 0 0 0 3px rgba(138, 85, 184, .22);
}
.chip:disabled { opacity: .5; cursor: default; }

/* ---- account list ---- */
.accounts-head { display: flex; align-items: baseline; justify-content: space-between; }
.count { color: var(--dim); font-size: .78rem; }
#userSearch { margin: 0 0 .5rem; padding: .4rem .55rem; font-size: .82rem; }
.acct { border: 1px solid var(--line); border-radius: 8px; margin-bottom: .3rem; overflow: hidden; }
.acct-head {
  width: 100%; display: flex; align-items: center; justify-content: space-between;
  gap: .6rem; background: transparent; border: 0; color: var(--text);
  padding: .42rem .65rem; font-size: .82rem; font-weight: 600; cursor: pointer; text-align: left;
}
.acct-head:hover { background: var(--card-2); filter: none; }
.acct[data-open="1"] .acct-head { background: var(--card-2); }
.acct-time { color: var(--dim); font-size: .72rem; font-weight: 400; white-space: nowrap; }
.acct-body { padding: .1rem .65rem .6rem; display: grid; gap: .5rem; }
/* An explicit display beats the [hidden] attribute, which is what kept every
   account expanded. */
.acct-body[hidden] { display: none; }
.acct-body .danger { justify-self: start; }
.empty { color: var(--dim); font-size: .85rem; padding: .5rem 0; }

h3.group {
  display: flex; align-items: baseline; gap: .4rem;
  font-size: .7rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--dim); margin: .9rem 0 .35rem; font-weight: 700;
}
h3.group:first-child { margin-top: .2rem; }
h3.group .count { font-size: .7rem; }
.chip.role { border-style: dashed; }
.chip.role.on { border-style: solid; }

.pwrow { display: flex; gap: .3rem; align-items: center; }
.pwfield {
  flex: 1; min-width: 0; margin: 0; padding: .26rem .5rem;
  font-size: .75rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.mini {
  width: auto; background: var(--card-2); border: 1px solid var(--line); color: var(--text);
  padding: .26rem .6rem; border-radius: 6px; font-size: .72rem; font-weight: 600;
  cursor: pointer; white-space: nowrap;
}
.mini:hover { border-color: var(--lav); filter: none; }

/* ---- the cat ---- */


/* The cat draws onto a full-viewport overlay rather than a box of its own, so
   a stretch can reach any corner of the screen with nothing to clip against.
   The slot below holds its place in the layout; the canvas itself is inert to
   the pointer, and cat.js decides whether a click landed on actual cat pixels
   before taking it. */
/* Absolutely positioned in the document rather than fixed to the viewport.
   Fixed meant the canvas stayed put while the page scrolled, so every frame had
   to redraw the cat at his new screen position. On mobile that redraw happens on
   the main thread while the scroll itself runs on the compositor, so he visibly
   trailed the page. In document space the browser scrolls him along with
   everything else, for free and perfectly in step. */
#cat {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 9999;
  pointer-events: none;
}

.cat-slot {
  width: 480px;
  height: 480px;
  margin: 0 auto;
  cursor: grab;
  touch-action: none;
}
.cat-slot:active { cursor: grabbing; }
#cat:active { cursor: grabbing; }

.greet {
  margin: .1rem 0 0;
  color: var(--text);
  font-size: 1.05rem;
  font-weight: 600;
}

/* ---- password reveal ---- */
/* The wrapper has to be exactly the input's box, or centring the button in it
   centres against the wrong thing: the input carries its own top margin and,
   as an inline-block, leaves baseline space underneath. Move the margin onto
   the wrapper and make the input a block, and top:50% lands true. */
.pw {
  position: relative;
  display: block;
  margin-top: .35rem;
}
.pw input {
  display: block;
  margin-top: 0;
  padding-right: 2.1rem;
}

.pw-toggle {
  position: absolute;
  right: .35rem;
  top: 50%;
  transform: translateY(-50%);
  width: auto;
  padding: .25rem;
  background: none;
  border: 0;
  border-radius: 6px;
  color: var(--dim);
  display: flex;
  align-items: center;
  cursor: pointer;
}
.pw .pw-toggle {
  background: none;
  border: 0;
  width: auto;
  color: var(--lav);
}
.pw .pw-toggle:hover { background: none; color: var(--lav-deep); }
.pw-toggle svg { display: block; }


/* The handwriting belongs on the login, where it reads as personality. The
   admin board is a dense table of names, dates and toggles, and a marker face
   makes that harder to scan, so it keeps the system stack. */
#admin {
  font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
}

/* Shantell sits a little small next to a system sans at the same size, and the
   greeting is the one line that should feel handwritten. */
.greet {
  font-size: 1.15rem;
  font-weight: 700;
}

/* ---- the cat, speaking ----
   The tail points left, so the cat stands on that side and the bubble grows to
   the right of him. Padding is in pixels rather than percentages because a
   percentage padding on a flex item resolves against the container, not the
   element, which makes it unpredictable here. The numbers come from measuring
   the drawing: its fill leaves roughly 25% clear on the left, 17% right and 18%
   top and bottom, so content stays off the linework. */
/* Nothing paints until the bubble image and the web font are both in hand.
   Otherwise the parts arrive separately: a big "meow" renders in the fallback
   face and then jumps when Shantell Sans loads, and the bubble pops in behind
   it. Holding them and fading in together makes the entrance deterministic. */
.talk, #cat {
  opacity: 0;
  transition: opacity .3s ease;
  /* Safety net: if the script never runs, this reveals everything anyway after
     a couple of seconds. A login page that stays invisible because of one bad
     line of JavaScript is not an acceptable failure. */
  animation: reveal .3s ease 2.2s forwards;
}
body.ready .talk, body.ready #cat { opacity: 1; animation: none; }
@keyframes reveal { to { opacity: 1; } }

/* The cat and his caption travel together; the drop that lines his mouth up
   with the tail belongs to the pair. */
.cat-side {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: 147px;
}

.cat-hint {
  margin: .4rem 0 0;
  color: var(--dim);
  font-size: .8rem;
  opacity: .75;
  transition: opacity .4s ease;
  user-select: none;
}
/* Once you have worked out that he is grabbable, the caption stops earning its
   place. */
.cat-hint.done { opacity: 0; }

.talk {
  display: flex;
  align-items: flex-start;     /* the tail is aligned by hand, not by centring */
  justify-content: center;
  margin-bottom: 1rem;
}

.bubble {
  flex: none;
  width: 800px;
  max-width: 100%;
  /* Held at the drawing's own proportions, so the shape is never stretched and,
     more usefully, its height is predictable. The tail sits 72.3% down the
     drawing and the cat's nose 64.4% down his, which centring cannot line up;
     with a known height the offset is just arithmetic. */
  aspect-ratio: 948 / 748;
  display: flex;
  flex-direction: column;
  justify-content: center;
  margin-left: 14px;               /* a little air between him and the tail */
  padding: 0 142px 0 216px;
  background: url('/bubble.png') no-repeat center;
  background-size: 100% 100%;
  color: #2b2a33;                  /* the same ink the drawing is outlined in */
}

/* The bubble's interior is far wider than a login form should be, so the
   contents keep a sane column and centre in it. */
.bubble > * { max-width: 340px; margin-left: auto; margin-right: auto; }

/* The cat says hello before handing over the form. Both live in the same place
   and cross-fade, and the form is laid out from the start rather than appearing
   afterwards, so nothing shifts and the bubble never changes size. */
.bubble { position: relative; }

.meow {
  position: absolute;
  inset: 0;
  max-width: none;
  margin: 0;
  /* An absolutely positioned box lays out against the padding box, so inset: 0
     spans the whole bubble and centres on its middle. The form centres inside
     the content box, which the tail pushes well to the right. Inheriting the
     padding gives the meow the same content box, so the two agree. */
  padding: inherit;
  display: grid;
  place-items: center;
  font-size: 3.6rem;
  font-weight: 700;
  color: var(--line-ink);
  pointer-events: none;
  transition: opacity .35s ease;
}
.meow.gone { opacity: 0; }

.bubble-body {
  opacity: 0;
  transition: opacity .35s ease;
  /* Same safety net as .talk: if the script dies before adding .shown, the form
     still arrives. Previously only .talk was covered, so one bad line of
     JavaScript left a bubble that opened and stayed empty. */
  animation: reveal .3s ease 2.4s forwards;
  /* The login and the account panel are stacked in the same grid cell, so the
     bubble is always as tall as the taller of the two and swapping them cannot
     change any height. Hiding one with display:none would drop it out of the
     layout and the difference would come back, so the inactive panel is only
     made invisible. */
  display: grid;
}
.bubble-body > * { grid-area: 1 / 1; }
.bubble-body > .off {
  visibility: hidden;
  pointer-events: none;
}
.bubble-body.shown { opacity: 1; animation: none; }

/* Anyone who has asked for less movement gets the form immediately. */
@media (prefers-reduced-motion: reduce) {
  .meow { display: none; }
  .bubble-body { opacity: 1; transition: none; }
}

/* The signed-in panel sits in the bubble too, so it takes the same light
   treatment as the login. */
.bubble h2 {
  color: var(--line-ink);
  text-align: center;
  font-size: 1.15rem;
  margin: 0 0 1rem;
}
.bubble button.secondary {
  background: transparent;
  border: 1px solid #d8c3e6;
  color: #6f5f80;
}
.bubble button.secondary:hover {
  background: transparent;
  border-color: var(--lav-deep);
  color: var(--lav-deep);
}

/* Inside the bubble everything flips: dark on light. The palette is taken from
   the drawings rather than invented: the ink is the outline colour, the accents
   are the lavender of the cat's ears. A near-black button sat on the soft pink
   like a bruise. */
.bubble .greet {
  color: var(--line-ink);
  margin: 0 auto .8rem;            /* auto, or it sits left of the form */
  font-size: 1.4rem;
  font-weight: 700;
  text-align: center;
}
.bubble label { color: #6f5f80; font-size: .95rem; }
.bubble input {
  background: #fff;
  border-color: #d8c3e6;
  color: var(--line-ink);
  font-size: 1.05rem;
}
.bubble input:focus { border-color: var(--lav); }   /* his ears */
.bubble .pw .pw-toggle { color: var(--lav-deep); }
.bubble .pw .pw-toggle:hover { color: var(--lav-deeper); }

/* The button borrows the bubble's own outline ink, which reads as part of the
   drawing. Lavender would only manage 2.8:1 against this fill. */
.bubble button { font-size: 1.05rem; padding: .75rem; }

.bubble .toggle { color: #6f5f80; margin: .9rem auto 0; font-size: .95rem; }
.bubble .toggle a { color: var(--lav-deep); }
.bubble .msg { color: #b03a3a; margin: .6rem auto 0; }
.bubble .msg.ok { color: #2f7a52; }

/* The drawing's proportions are held by scaling every measurement together. */
@media (max-width: 1320px) {
  .cat-slot { width: 380px; height: 380px; }
  .cat-side { margin-top: 109px; }
  .bubble { width: 620px; padding: 0 110px 0 167px; margin-left: 12px; }
  .card { max-width: 1060px; }
}

@media (max-width: 1080px) {
  .cat-slot { width: 300px; height: 300px; }
  .cat-side { margin-top: 92px; }
  .bubble { width: 500px; padding: 0 89px 0 135px; margin-left: 10px; }
  .card { max-width: 820px; }
}

/* Stacked, so a bubble whose tail points sideways makes no sense: it uses a
   copy of the drawing rotated a quarter turn, tail upwards, and the cat sits
   over it.
   Crucially the fixed aspect ratio is dropped here. On a narrow screen it
   forced a height shorter than the form needed and the text spilled out of the
   drawing; letting the content set the height fixes that. Padding is in
   percentages, which resolve against this width, so it tracks the bubble at any
   size. The values come from measuring the rotated art: 18.2% clear on the
   left, 19% right, and 25.3% / 16.2% of its height top and bottom, converted
   into width terms. */
@media (max-width: 840px) {
  /* Here the bubble is sized by its content, so swapping the login for the
     taller account panel changes its height. Centred vertically that reads as
     the whole page jumping upward, so this layout anchors to the top and lets
     the growth go downwards instead. */
  .page { place-items: start center; }
  body { padding-top: 1.15rem; }

  .talk { flex-direction: column; align-items: stretch; }
  .card { max-width: 420px; }

  .cat-side {
    align-self: flex-start;
    /* Centres him over the tail, which sits 27.6% across the rotated drawing.
       Unlike the side-by-side layout, the caption sits between the cat and the
       bubble here, so the gap below has to clear the arrow tip rather than close
       up against it. The cat rides higher (see the reduced padding above) so
       buying that clearance does not push the login further down the screen. */
    margin: 0 0 12px calc(27.6% - 110px);
  }
  .cat-slot { width: 220px; height: 220px; }

  .bubble {
    width: 100%;
    aspect-ratio: auto;
    background-image: url('/bubble-up.png');
    margin-left: 0;
    padding: 32% 19% 21% 18.2%;
  }

  .bubble .greet { font-size: 1.15rem; }
  .meow { font-size: 2.6rem; }
}

@media (max-width: 420px) {
  .cat-slot { width: 180px; height: 180px; }
  .cat-side { margin-left: calc(27.6% - 90px); }
}
