:root {
  /* Two families, taken off the artwork: the bottle's cyan light, and the warm
     paper/cork/rope of the message inside it. */
  --sand: #f3e7cf;
  --sand-dim: #b9c9da;
  --sea-deep: #06152b;
  --sea: #0e2f52;
  --cyan: #46d9ff;
  --cyan-bright: #a9f0ff;
  --gold: #e8b45a;
  --gold-bright: #ffd489;
  --panel: rgba(7, 27, 50, 0.62);
  --panel-line: rgba(169, 240, 255, 0.20);
  --danger: #ff9a8b;
  --ok: #8fe3b0;
  --radius: 6px;
  --display: Underdog, Georgia, serif;
}

* { box-sizing: border-box; }

/* class-based display rules must not beat the hidden attribute */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  min-height: 100%;
  /* the bob tilt and the cast animation both swing the bottle past the gutter */
  overflow-x: clip;
}

body {
  font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
  color: var(--sand);
  background: var(--sea-deep);
  -webkit-font-smoothing: antialiased;
}

/* The in-game shoreline, held still behind the page. A fixed element rather than
   background-attachment: fixed, which iOS Safari does not honour with cover. */
.sky {
  position: fixed;
  inset: 0;
  z-index: -1;
  background: url("/assets/sea.webp") center / cover no-repeat var(--sea-deep);
}

/* Scrim: sinks the shoreline far enough for text to sit on it, and pools cyan
   light where the bottle hangs. */
.sky::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(36rem 24rem at 50% 38%, rgba(70, 217, 255, 0.20), transparent 70%),
    linear-gradient(
      180deg,
      rgba(6, 21, 43, 0.78) 0%,
      rgba(6, 21, 43, 0.34) 26%,
      rgba(6, 21, 43, 0.52) 62%,
      rgba(4, 14, 30, 0.90) 100%
    );
}

.wrap {
  max-width: 44rem;
  margin: 0 auto;
  padding: 3rem 16px 4rem;
  display: flex;
  flex-direction: column;
  gap: 1.75rem;
}

/* Masthead ---------------------------------------------------------------- */

.masthead { text-align: center; }

.kicker {
  margin: 0 0 .35rem;
  font-size: .75rem;
  letter-spacing: .28em;
  text-transform: uppercase;
  color: var(--cyan);
  opacity: .9;
}

h1 {
  font-family: var(--display);
  margin: 0;
  font-size: clamp(2.1rem, 7.5vw, 3.4rem);
  line-height: 1.15;
  color: #fff;
  text-shadow:
    0 0 26px rgba(70, 217, 255, 0.55),
    0 2px 10px rgba(4, 14, 30, 0.8);
}

/*.tagline {
  margin: .6rem 0 0;
  color: var(--sand-dim);
  font-size: .95rem;
  text-shadow: 0 1px 8px rgba(4, 14, 30, 0.9);
}*/

/* Bottle ------------------------------------------------------------------ */

/* The drift the bottle on the stage rides. The art ships its own alpha and is
   trimmed to it, so the glass simply floats on the water — no frame, no plate
   behind it. */
@keyframes bob {
  0%, 100% { transform: translateY(0) rotate(-1deg); }
  50%      { transform: translateY(-10px) rotate(1deg); }
}

.banner {
  margin: 0;
  padding: .75rem 1rem;
  border-radius: 6px;
  border: 1px solid rgba(255, 154, 139, .4);
  background: rgba(255, 154, 139, .12);
  color: var(--danger);
  font-size: .9rem;
  text-align: center;
}

/* Panels ------------------------------------------------------------------ */

/* The game's UIBlur glass (BoomLagoon's air-raid timer wears it): whatever is
   live behind the panel, blurred and then downsampled into big squares
   (#htf-glass in the page, ~20px cells -- the material's _Pixel_Ratio). The
   plain blur comes first so a browser that will not take an SVG backdrop
   filter still gets frosted glass. */
.panel {
  background: rgba(7, 27, 50, 0.42);
  border: 1px solid var(--panel-line);
  border-radius: var(--radius);
  padding: 1.4rem;
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.3);
}

/* Only Firefox renders an SVG backdrop filter. Chromium and Safari accept the
   value and then draw no backdrop effect at all, so they keep the blur. */
@supports (-moz-appearance: none) {
  .panel,
  .locker { backdrop-filter: url(#htf-glass); }
}

.panel h2,
.gate h2 {
  margin: 0 0 .4rem;
  font-family: var(--display);
  font-size: 1.35rem;
  font-weight: 400;
  color: var(--cyan-bright);
}

.muted { color: var(--sand-dim); font-size: .88rem; margin: 0; }

.loading {
  display: flex;
  align-items: center;
  gap: .6rem;
  justify-content: center;
  color: var(--sand-dim);
}

.spinner {
  width: 1rem; height: 1rem;
  border: 2px solid rgba(169, 240, 255, .25);
  border-top-color: var(--cyan);
  border-radius: 50%;
  animation: spin .9s linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

/* Providers ---------------------------------------------------------------
   Signed out these fill the controls pane under the sheet, standing in for the
   envelope line and the cast button. */

.gate { text-align: center; }

.gate-title {
  margin: 0;
  font-family: var(--display);
  font-size: 1.55rem;
  font-weight: 400;
  letter-spacing: .01em;
  color: var(--gold-bright);
  text-shadow: 0 2px 12px rgba(232, 180, 90, .25);
}

.providers {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: .6rem;
  margin-top: 1.1rem;
}

.provider {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .6rem;
  padding: .8rem 1rem;
  border-radius: 6px;
  border: 1px solid transparent;
  font: inherit;
  font-weight: 600;
  color: #fff;
  text-decoration: none;
  transition: transform .12s ease, filter .12s ease;
}

.provider:hover { transform: translateY(-1px); filter: brightness(1.08); }
.provider:active { transform: translateY(0); }

.provider svg { width: 1.15rem; height: 1.15rem; fill: currentColor; }

.provider.discord { background: #5865f2; }
.provider.twitch  { background: #9146ff; }
.provider.steam   { background: #1b2838; border-color: rgba(169, 240, 255, .28); }

.gate-fine {
  margin: 1rem 0 0;
  font-size: .75rem;
  color: var(--sand-dim);
  opacity: .8;
}
.gate-fine a { color: var(--cyan-bright); }


/* Composer ---------------------------------------------------------------- */

/* Tighter than the other panels: the sheet inside it wants the room. */
#composer { padding: 1rem; }

/* Drawn up so the bottom of the sheet lies over its top edge -- the paper hangs
   in front of the chrome, not above it. */
.controls { margin-top: -2.5rem; }

/* Signed out there is nothing to write, so the sign-in pane leaves its place
   under the sheet and rests centred on top of it instead. app.js measures the
   rise into --gate-lift; the transform transition below makes it a glide, both
   in and back out once someone signs in. */
.controls.is-gated {
  position: relative;
  z-index: 2;
  transform: translateY(var(--gate-lift, 0px));
}

/* Identity moved into the locker; these dress it there. */
.avatar { border-radius: 50%; }

.whoami-text { display: flex; flex-direction: column; line-height: 1.25; }
.whoami-text .muted { font-size: .78rem; }

.linkish {
  margin-left: auto;
  background: none;
  border: 0;
  color: var(--sand-dim);
  font: inherit;
  font-size: .85rem;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.linkish:hover { color: var(--sand); }

/* The sheet needs no heading -- it plainly is the message. The label stays for
   screen readers, which cannot see that. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The sheet ----------------------------------------------------------------
   Paper art, the ink memo.js draws over it, and the real textarea invisible on
   top. The three are stacked in one box the size of the art, so a stroke drawn
   at a grid cell lands where the typist expects it. */

/* The stage ----------------------------------------------------------------
   The empty bottle waiting on the water, with the sheet hanging over it. Both
   live outside the panel: the page is the paper, and a pane behind it would
   only put a box between the note and the sea. */

.stage {
  position: relative;
  display: flex;
  justify-content: center;
  /* The bottle is wider and taller than the sheet and is not in the flow, so
     the stage claims almost no room of its own: the art overhangs both ends and
     the panel below is drawn up under the paper. The little top padding that is
     left is the gap that keeps the glass clear of the masthead. */
  padding: 2rem 0 0;
  margin: -.5rem 0 0;
}

.stage-drift {
  position: relative;
  display: flex;
  justify-content: center;
  width: 100%;
}

.stage-art {
  position: absolute;
  /* Offset up and to the left, along the axis the bottle is tilted on: that
     keeps the cork, neck and body of the glass clear of the sheet and leaves
     only the base behind it, so the bottle reads as one object lying under the
     page instead of a shape cut in half by it. The top padding on .stage is the
     room that overhang needs to clear the masthead. */
  inset: -5.5rem 3.5rem 3.5rem -3.5rem;
  /* One cell, both bottles in it: centring is the grid's job, which leaves the
     transform free for the bob. */
  display: grid;
  place-items: center;
  pointer-events: none;
  animation: bob 7s ease-in-out infinite;
}

.stage-bottle {
  grid-area: 1 / 1;
  display: block;
  width: min(35rem, calc(100vw - 2rem));
  height: auto;
  filter: drop-shadow(0 0 30px rgba(70, 217, 255, 0.40))
          drop-shadow(0 0 80px rgba(70, 217, 255, 0.22));
}

/* The filled bottle is stacked on the empty one and shows only for the moment
   the note is sealed into it. */
.stage-bottle.full {
  opacity: 0;
  transition: opacity .4s ease;
}

.stage.is-sealing .stage-bottle.full { opacity: 1; }
.stage.is-sealing .stage-bottle.empty { opacity: 0; transition: opacity .4s ease; }

.memo {
  position: relative;
  /* Raised so it sits over the belly of the bottle rather than beside it, and
     kept narrower than the glass so the bottle still reads as the thing behind
     the page. The height cap keeps a tall sheet on a short screen from pushing
     the send button out of reach; 591/745 is the art's ratio. */
  z-index: 1;
  width: min(26.5rem, 62vw);
  margin: 0 auto;
  transform: translateY(-4%);
  aspect-ratio: 591 / 745;
  background: url("/assets/paper.webp") center / contain no-repeat;
  /* The sheet is lit by the same cyan as everything else on the page. */
  filter: drop-shadow(0 12px 22px rgba(3, 12, 26, .55));
  opacity: 0.965;
}

.memo-ink {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* The textarea covers only the ink area, not the whole sheet: memo.js sets its
   position, size, font and letter-spacing so its lines and characters land on
   the same grid the strokes are drawn on, and a selection highlights the words
   it is actually under. Until that runs it is simply the sheet. */
.memo textarea {
  position: absolute;
  inset: 0;
}

.memo-ink { pointer-events: none; }

/* Unwritten sheets carry the placeholder in the same hand, faded. */
.memo.is-placeholder .memo-written { opacity: .38; }

/* Past the last line the paper holds, the words are lost -- say so in the ink. */
.memo-ink.is-overflowing .memo-written { stroke: #8a4a2a; }

.memo-caret { animation: blink 1.06s steps(1, end) infinite; }
.memo:not(.is-writing) .memo-caret { display: none; }

@keyframes blink { 50% { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  .memo-caret { animation: none; }
}

/* Invisible, but still the thing being typed into: the caret and the text are
   drawn on the sheet instead. */
.memo textarea {
  margin: 0;
  padding: 0;
  border: 0;
  resize: none;
  background: transparent;
  color: transparent;
  caret-color: transparent;
  font: inherit;
}

.memo textarea::selection { background: rgba(110, 55, 18, .22); }
.memo textarea::placeholder { color: transparent; }

/* The sheet is deliberately ringless: a rectangle drawn around torn paper reads
   as a box the page does not have. The caret on the paper is the focus cue. */
.memo textarea:focus-visible,
.memo textarea:focus { outline: none; }

textarea:focus-visible,
.send:focus-visible,
.provider:focus-visible,
.linkish:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 2px;
}

/* Bookkeeping, not the point of the page: it sits down by the footer, small, so
   the sheet keeps the panel to itself. */
.meter {
  display: flex;
  justify-content: center;
  gap: .5rem .9rem;
  flex-wrap: wrap;
  margin: -1.1rem 0 0;
  font-size: .74rem;
  color: rgba(185, 201, 218, .7);
  text-shadow: 0 1px 6px rgba(4, 14, 30, .9);
  font-variant-numeric: tabular-nums;
}

/* A dot between the two, when both are there. */
.meter .quota::before { content: "·"; margin-right: .9rem; }

/* Sits under the meter, above the send button: quiet, because drifting to
   anyone is the default and addressing it is the exception. */
.signing {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  margin: .9rem 0 .9rem;
  font-size: .82rem;
  color: var(--sand-dim);
  cursor: pointer;
}

.signing input { accent-color: var(--cyan); cursor: pointer; }

.signing input:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 2px;
}

/* The fold. max-height rather than a measured height: the envelope line is one
   label, one input and one hint, so a ceiling well above that unrolls at a
   steady speed and needs nothing from script. `hidden` is dropped a frame
   before .is-open goes on, which is what gives the first open something to
   animate from. */
/* Checkbox and envelope line share one row; the line unrolls sideways. */
.targeting {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: .9rem;
  position: relative;
  z-index: 2;
  margin: 0 0 .5rem;
}

.targeting[hidden] { display: none; }

.targeting .signing { margin: 0; }

.addressed-wrap {
  overflow: hidden;
  max-width: 0;
  opacity: 0;
  transition: max-width .35s ease, opacity .25s ease;
}

.addressed-wrap.is-open {
  overflow: visible;
  max-width: 16rem;
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .addressed-wrap { transition: none; }
}

/* The envelope line. Quieter than the sheet and the send button: addressing a
   bottle is the exception, and an empty box has to read as the normal thing. */
.addressed {
  display: flex;
  align-items: center;
  gap: .45rem;
  width: 14rem;
  font-size: .82rem;
  color: var(--sand-dim);
}

.info {
  flex: none;
  position: relative;
  display: inline-grid;
  place-items: center;
  width: 1rem;
  height: 1rem;
  border: 1px solid currentColor;
  border-radius: 50%;
  font: italic 600 .66rem/1 Georgia, serif;
  cursor: help;
  opacity: .8;
}

.info:focus { outline: none; }

.info-tip {
  position: absolute;
  bottom: calc(100% + .5rem);
  right: -.4rem;
  z-index: 5;
  width: 15rem;
  max-width: calc(100vw - 2rem);
  padding: .55rem .7rem;
  border-radius: 6px;
  font-style: normal;
  font-weight: 400;
  font-size: .76rem;
  line-height: 1.4;
  font-family: system-ui, sans-serif;
  color: var(--sand);
  background: rgba(10, 20, 28, .95);
  box-shadow: 0 4px 14px rgba(0, 0, 0, .35);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity .15s ease;
}

.info:hover .info-tip,
.info:focus .info-tip { opacity: 1; visibility: visible; }

.addressed input {
  flex: 1;
  min-width: 0;
  padding: .55rem .7rem;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 6px;
  font: inherit;
  font-size: .9rem;
  color: var(--sand);
  background: rgba(0, 0, 0, 0.2);
}

.addressed input::placeholder { color: var(--sand-dim); opacity: .65; }

/* No focus ring, same as the memo: the caret is the focus cue. */
.addressed input:focus,
.addressed input:focus-visible { outline: none; }

.addressed input:disabled { opacity: .5; cursor: not-allowed; }

/* A picked player: the name stands in for their SteamID64, so it reads as set. */
.addressed input.is-picked { border-color: var(--cyan); color: var(--cyan-bright); }

/* The player search list, dropped under the envelope line. */
.addressed { position: relative; }

.steam-results {
  position: absolute;
  top: calc(100% + .3rem);
  left: 0;
  right: 0;
  z-index: 6;
  max-height: 16rem;
  overflow-y: auto;
  margin: 0;
  padding: .25rem;
  list-style: none;
  border: 1px solid var(--panel-line);
  border-radius: var(--radius);
  font-family: system-ui, sans-serif;
  background: rgba(10, 20, 28, .97);
  box-shadow: 0 6px 18px rgba(0, 0, 0, .45);
}

.steam-results[hidden] { display: none; }

.steam-option {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .35rem .45rem;
  border-radius: 4px;
  color: var(--sand);
  cursor: pointer;
}

.steam-option:hover,
.steam-option[aria-selected="true"] { background: rgba(70, 217, 255, .16); }

.steam-option img {
  flex: none;
  width: 24px;
  height: 24px;
  border-radius: 3px;
}

.steam-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.steam-id {
  flex: none;
  font-size: .72rem;
  color: var(--sand-dim);
  opacity: .7;
}

.steam-note {
  padding: .4rem .5rem;
  font-size: .78rem;
  color: var(--sand-dim);
}

.count.near { color: var(--gold-bright); }
.quota.none { color: var(--danger); }

/* Sits in the sheet's footer, below the ink area, above the invisible textarea. */
.memo .send {
  position: absolute;
  left: 8%;
  right: 8%;
  bottom: 7%;
  z-index: 2;
  width: auto;
  padding: .55rem 1rem;
  font-size: .95rem;
}

.memo .send[hidden] { display: none; }

.send {
  width: 100%;
  padding: .9rem 1rem;
  border: 0;
  border-radius: 6px;
  font: inherit;
  /* Warm against all that cyan — it belongs to the paper and rope in the bottle,
     and keeps the one action on the page from disappearing into the water. */
  font-family: var(--display);
  font-size: 1.05rem;
  letter-spacing: .02em;
  color: #2a1c06;
  background: transparent;
  cursor: pointer;
  transition: transform .12s ease, filter .12s ease, opacity .2s ease, visibility .2s;
}

.send:hover:not(:disabled) { transform: translateY(-1px); filter: brightness(1.05); }

/* Only there when it can be pressed -- except while it is busy sealing, when
   it says so. */
.send:disabled:not([data-busy="1"]) { opacity: 0; visibility: hidden; }
.send[data-busy="1"] { cursor: progress; }

/* Footer ------------------------------------------------------------------ */

.foot {
  text-align: center;
  font-size: .78rem;
  color: rgba(185, 201, 218, .6);
  text-shadow: 0 1px 6px rgba(4, 14, 30, .9);
}

/* Sent animation ---------------------------------------------------------- */

/* Three beats, driven from app.js: the sheet is rolled into the bottle, the
   full bottle is cast out to sea, and a fresh empty bottle drifts in with a
   clean sheet over it. */

.stage.is-sealing .memo { animation: seal .7s ease-in forwards; }

@keyframes seal {
  0%   { transform: translateY(-17%) scale(1) rotate(0); opacity: 1; }
  60%  { transform: translateY(-6%) scale(.42) rotate(-8deg); opacity: .85; }
  100% { transform: translateY(4%) scale(.12) rotate(-14deg); opacity: 0; }
}

.stage.is-casting .stage-drift { animation: cast .85s ease-in forwards; }

@keyframes cast {
  0%   { transform: translate(0, 0) rotate(0) scale(1); opacity: 1; }
  100% { transform: translate(34vw, -34vh) rotate(38deg) scale(.3); opacity: 0; }
}

/* The replacement comes in from the other side, so it reads as a new bottle
   drifting up rather than the same one bouncing back. */
.stage.is-arriving .stage-drift { animation: arrive .8s cubic-bezier(.2, .8, .3, 1) both; }

@keyframes arrive {
  0%   { transform: translate(-34vw, 12vh) rotate(-22deg) scale(.55); opacity: 0; }
  100% { transform: translate(0, 0) rotate(0) scale(1); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .stage-art { animation: none; }
  .stage.is-sealing .memo,
  .stage.is-casting .stage-drift,
  .stage.is-arriving .stage-drift { animation: none; }
  /* Still says something happened, without the flight. */
  .stage.is-casting .stage-drift { opacity: 0; transition: opacity .3s ease; }
}

/* Arrival ------------------------------------------------------------------
   The page opens on the water alone. Nothing in the column is drawn until
   /api/me has answered, because which half of it belongs there -- the gate or
   the composer -- is not known until then, and a masthead that appears first
   only to have a bottle pop in under it a moment later reads as two loads.

   So .is-booting (set in the markup, dropped by app.js once the section is
   unhidden) holds the whole column back, and letting it go plays the arrival:
   the glass fades up out of the sea while the sheet and the chrome under it
   rise into place beside it.

   Transitions rather than keyframes, deliberately: the bottle is already
   running `bob` and the sheet is one `seal` away from being animated itself,
   and an animation would either have to be composed with those or torn off
   afterwards. A transition loses to an animation, so the cast simply takes
   over when its time comes, and nothing has to be cleaned up. */

.masthead,
.foot,
.meter,
.stage-art,
.controls,
.memo {
  transition: opacity .9s ease, transform .95s cubic-bezier(.2, .8, .3, 1);
}

/* The bottle has nowhere to come from -- it is the thing behind the page, and
   it is simply lit. Only its opacity is touched, so the bob is left alone. */
body.is-booting .masthead,
body.is-booting .foot,
body.is-booting .meter,
body.is-booting .stage-art { opacity: 0; }

/* The paper and the chrome under it are laid onto the scene together, from
   below. The sheet keeps its resting -4% in the offset it starts from, so it
   lands exactly where it lives rather than snapping at the end. */
body.is-booting .controls {
  opacity: 0;
  transform: translateY(2.75rem);
}

body.is-booting .controls.is-gated {
  transform: translateY(calc(var(--gate-lift, 0px) + 2.75rem));
}

body.is-booting .memo {
  opacity: 0;
  transform: translateY(calc(-4% + 2.75rem));
}

/* A slow tide still says something -- after long enough that a quick one never
   shows a flash of it. */
body.is-booting .loading { animation: waiting-in .6s ease .9s both; }

@keyframes waiting-in { from { opacity: 0; } to { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  body.is-booting .controls { transform: none; }
  body.is-booting .controls.is-gated { transform: translateY(var(--gate-lift, 0px)); }
  body.is-booting .memo { transform: translateY(-4%); }
}

/* The locker ---------------------------------------------------------------
   A side nav for the visitor's own bottles: collapsed it is a tab on the left
   edge, expanded it is a panel over the page. It overlays rather than pushing
   the page aside, so the sheet never moves out from under a typist's hands.

   It is never given the hidden attribute while it is on screen -- that would
   kill the slide, since [hidden] is display: none !important above. Closed
   means translated off-canvas; hidden is only for a signed-out visitor, who has
   no locker at all. */

/* The menu button, where a menu button belongs -- top-left, at every width --
   but wearing the visitor's own face instead of three lines. The avatar is the
   whole button, so it is round and the size of a comfortable tap target. */
.locker-toggle {
  position: fixed;
  z-index: 3;
  top: .8rem;
  left: .8rem;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border-radius: 50%;
  display: grid;
  place-items: center;
  border: 1px solid var(--panel-line);
  background: var(--panel);
  backdrop-filter: blur(10px);
  color: var(--cyan-bright);
  font: inherit;
  cursor: pointer;
  /* The same cyan the bottle casts, so the face reads as part of the page. */
  box-shadow: 0 6px 18px rgba(0, 0, 0, .4), 0 0 0 1px rgba(70, 217, 255, .12);
  transition: transform .14s ease, filter .14s ease;
}

.locker-toggle:hover { transform: scale(1.06); filter: brightness(1.1); }
.locker-toggle:active { transform: scale(1); }

.locker-toggle-face,
.locker-toggle-initial {
  grid-area: 1 / 1;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  object-fit: cover;
}

/* No avatar from the provider: the first letter, in the same circle. */
.locker-toggle-initial {
  display: grid;
  place-items: center;
  background: rgba(70, 217, 255, .16);
  font-family: var(--display);
  font-size: 1.1rem;
  color: var(--cyan-bright);
}

.locker-toggle:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 3px;
}

/* How many bottles are in there, pinned to the corner of the face. Blank until
   the list has actually been read -- the count is not known on page load, by
   design. */
.locker-badge {
  position: absolute;
  top: -.2rem;
  right: -.2rem;
  min-width: 1.15rem;
  padding: 0 .25rem;
  border-radius: 999px;
  border: 2px solid var(--sea-deep);
  background: var(--cyan);
  color: #04202c;
  font-size: .66rem;
  font-weight: 600;
  line-height: 1.05rem;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* Off the button, but not gone: an avatar is a picture of a person, not a word,
   so this is the only thing naming the control to a screen reader. */
.locker-toggle-label {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.locker-backdrop {
  position: fixed;
  inset: 0;
  z-index: 4;
  background: rgba(3, 11, 24, .6);
  opacity: 0;
  visibility: hidden;
  transition: opacity .22s ease, visibility .22s ease;
}

.locker-backdrop.is-open { opacity: 1; visibility: visible; }

@media (max-width: 60rem) {
  /* On a phone the drawer covers the page, so scrolling behind it is scrolling
     something nobody can see. The locker's own list scrolls inside it. */
  body.locker-open { overflow: hidden; }

  /* The button sits in the corner the kicker line reaches on a narrow screen,
     so the masthead starts below it rather than under it -- but only while
     somebody is signed in and the button is actually there. */
  body.has-locker .wrap { padding-top: 4.4rem; }
}

.locker {
  position: fixed;
  z-index: 5;
  top: 0;
  left: 0;
  bottom: 0;
  width: min(22rem, 88vw);
  display: flex;
  flex-direction: column;
  /* The same glass as .panel (the Firefox pixel filter is applied with it). */
  background: rgba(7, 27, 50, 0.42);
  border-right: 1px solid var(--panel-line);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  box-shadow: 18px 0 50px rgba(0, 0, 0, .45);
  transform: translateX(-100%);
  visibility: hidden;
  transition: transform .26s cubic-bezier(.2, .8, .3, 1), visibility .26s;
}

.locker.is-open { transform: none; visibility: visible; }

@media (prefers-reduced-motion: reduce) {
  .locker, .locker-backdrop { transition: none; }
}

/* Who the locker belongs to, at the top of it: the same face that is on the
   button, now with a name beside it. */
.locker-who {
  display: flex;
  align-items: center;
  gap: .7rem;
  padding: 1.1rem 1rem;
  border-bottom: 1px solid var(--panel-line);
}

.locker-who .avatar {
  flex: none;
  width: 2.75rem;
  height: 2.75rem;
  object-fit: cover;
  box-shadow: 0 0 0 1px rgba(70, 217, 255, .25);
}

.locker-who .whoami-text { min-width: 0; }

/* Name with the sign-in provider trailing it, small and muted. */
.locker-who .whoami-name {
  display: flex;
  align-items: baseline;
  gap: .4rem;
  min-width: 0;
}

.locker-who #viaProvider {
  flex: none;
  font-size: .75rem;
  color: grey;
}

/* Long display names are common and must not push the × off the edge. */
.locker-who #username {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.locker-who #logout,
.locker-who #adminLink {
    margin-left: 0;
    padding: 0;
    font-size: .8rem;
    text-align: left
}

/* The list's own heading, with today's allowance beside it. */
.locker-head #lockerQuota {
  margin-left: auto;
  font-size: .85rem;
}

.locker-head {
  display: flex;
  align-items: baseline;
  gap: .6rem;
  padding: .9rem 1rem .2rem;
}

.locker-head h2 {
  margin: 0;
  font-family: var(--display);
  font-size: 1.25rem;
  font-weight: 400;
  color: var(--cyan-bright);
}


.locker-close {
  margin-left: auto;
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: rgba(255, 255, 255, .06);
  color: var(--sand-dim);
  cursor: pointer;
}

.locker-close:hover { background: rgba(255, 255, 255, .12); color: var(--sand); }
.locker-close svg { width: 1rem; height: 1rem; fill: currentColor; }

.locker-close:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 2px;
}

/* The list scrolls; the header stays. */
.locker-body {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* A light system scrollbar glares on the glass. */
  scrollbar-width: thin;
  scrollbar-color: rgba(169, 240, 255, .25) transparent;
  padding: 1rem;
}

.locker-note {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin: .5rem 0;
  font-size: .86rem;
  color: var(--sand-dim);
}

.locker-note.is-error { color: var(--danger); }
.locker-note.is-quiet { justify-content: center; font-size: .78rem; opacity: .7; }

/* "Today" over the allowance, "Earlier days" over what went out before the
   reset -- small caps in the masthead kicker's voice, so they label rather
   than shout. */
.locker-day {
  margin: 1.1rem 0 .55rem;
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--cyan);
  opacity: .8;
}

.locker-day:first-child { margin-top: 0; }

/* One bottle ---------------------------------------------------------------- */

.bottle-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: .7rem;
}

.bottle-card,
.bottle-slot {
  padding: .8rem;
  border: 1px solid var(--panel-line);
  border-radius: 6px;
  background: rgba(7, 27, 50, .6);
  /* The bottle stands in its own column to the left of everything it is, and
     the confirmation drops across the full width of the card under both. */
  display: grid;
  grid-template-columns: 2.5rem 1fr;
  gap: .5rem;
  align-items: start;
}

.bottle-card {
  position: relative;
  /* Sinking a bottle collapses the card rather than snatching it away. */
  transition: opacity .3s ease, transform .3s ease;
}

.bottle-body { min-width: 0; }

.bottle-icon {
  width: 2.5rem;
  height: 2.5rem;
  object-fit: contain;
  /* The cast bottle carries the same cyan light the big one does, at the size
     a row can afford. */
  filter: drop-shadow(0 0 8px rgba(70, 217, 255, .45));
}

/* An unspent slot is the uncorked bottle with the colour drained out of it:
   the same glass, not yet anything. */
.bottle-icon.is-empty {
  filter: grayscale(1) brightness(1.2) opacity(.6);
}

/* Nothing has happened to a slot yet, so it is quieter than a card in every
   way -- dashed edge, no plate behind it, dimmer type. */
.bottle-slot {
  border-style: dashed;
  border-color: rgba(169, 240, 255, .16);
  background: rgba(7, 27, 50, .3);
  align-items: center;
}

.bottle-slot-text {
  margin: 0;
  font-size: .82rem;
  color: rgba(185, 201, 218, .8);
}

.bottle-slot-hint {
  margin: .15rem 0 0;
  font-size: .72rem;
  color: rgba(185, 201, 218, .55);
}

.bottle-card.is-confirming { border-color: rgba(255, 154, 139, .45); }

.bottle-card.is-sinking {
  opacity: 0;
  transform: translateY(14px) scale(.96);
  pointer-events: none;
}

/* The note as written, wrapping and line breaks kept -- it is the thing being
   listed, so it is not truncated. At 200 characters it does not need to be. */
.bottle-text {
  margin: .55rem 0;
  font-size: .86rem;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  color: var(--sand);
}

.bottle-facts {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: .35rem;
  margin: 0;
  padding: 0;
}

.tag {
    padding: .12rem .45rem;
    border-radius: 4px !important;
    border-width: 2px;
    border: 1px solid rgba(255, 255, 255, .14);
    background: rgba(0, 0, 0, .22);
    font-size: .7rem;
    color: var(--sand-dim);
    white-space: nowrap;
}

/* Awaiting approval is warm, adrift is green, and a find is the one
   thing worth being loud about. */
.tag.is-pending { color: var(--cyan-bright); border-color: var(--cyan-bright); }
.tag.is-adrift { color: var(--ok); border-color: var(--ok); }
.tag.is-rejected { color: var(--danger); border-color: rgba(255, 154, 139, .4); }

/* How many times it has been read, pinned to the card's top-right corner. The
   note's first line leaves room for it. */
.bottle-reads {
  position: absolute;
  top: .6rem;
  right: .7rem;
  font-size: .7rem;
  color: rgba(185, 201, 218, .75);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.bottle-reads.is-found { color: var(--ok); font-weight: 600; }

.bottle-card:has(.bottle-reads) .bottle-facts { padding-right: 3.5rem; }

/* No tags left (an open bottle at sea): the note is the first line, so it
   makes room for the read count itself. */
.bottle-facts:empty { display: none; }
.bottle-facts:empty + .bottle-text { margin-top: 0; }
.bottle-card:has(.bottle-reads) .bottle-facts:empty + .bottle-text { padding-right: 3.5rem; }

/* At sea: a green edge. */
.bottle-card.is-adrift:not(.is-confirming) { border-color: var(--ok); }

/* A note addressed to one player gets a gold edge, so it stands out from the
   bottles cast to anyone. */
.bottle-card.is-targeted:not(.is-confirming) { border-color: var(--gold-bright); }
.tag.is-addressed { color: var(--gold-bright); border-color: var(--gold-bright); }

.bottle-foot {
  display: flex;
  align-items: center;
  gap: .6rem;
  margin-top: .6rem;
}

.bottle-when { font-size: .72rem; color: rgba(185, 201, 218, .7); }

.bottle-delete {
  margin-left: auto;
  padding: .2rem .5rem;
  border: 1px solid transparent;
  border-radius: 6px;
  background: none;
  color: rgba(185, 201, 218, .75);
  font: inherit;
  font-size: .74rem;
  cursor: pointer;
}

.bottle-delete:hover {
  color: var(--danger);
  border-color: rgba(255, 154, 139, .35);
  background: rgba(255, 154, 139, .1);
}

.bottle-delete:focus-visible,
.bottle-keep:focus-visible,
.bottle-sink:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 2px;
}

/* Asked inside the card, so the bottle being sunk is still on screen next to
   the question. */
.bottle-confirm {
  grid-column: 1 / -1;
  margin-top: .6rem;
  padding-top: .6rem;
  border-top: 1px solid rgba(255, 154, 139, .25);
}

.bottle-confirm-text {
  margin: 0 0 .5rem;
  font-size: .76rem;
  color: var(--sand-dim);
}

.bottle-confirm-error {
  margin: .5rem 0 0;
  font-size: .74rem;
  color: var(--danger);
}

.bottle-confirm-buttons { display: flex; gap: .4rem; }

.bottle-keep,
.bottle-sink {
  flex: 1;
  padding: .4rem .5rem;
  border-radius: 6px;
  font: inherit;
  font-size: .76rem;
  cursor: pointer;
}

.bottle-keep {
  border: 1px solid rgba(255, 255, 255, .18);
  background: rgba(255, 255, 255, .06);
  color: var(--sand);
}

.bottle-keep:hover { background: rgba(255, 255, 255, .12); }

.bottle-sink {
  border: 1px solid rgba(255, 154, 139, .5);
  background: rgba(255, 154, 139, .16);
  color: var(--danger);
  font-weight: 600;
}

.bottle-sink:hover:not(:disabled) { background: rgba(255, 154, 139, .26); }
.bottle-sink:disabled { opacity: .6; cursor: not-allowed; }

/* Room enough that the panel misses the 44rem column: the page stays readable
   with the locker open, so it is not dimmed out -- and, since it is not covered
   either, the backdrop must stop swallowing clicks meant for the page. Closing
   is then the button, the × or Escape; there is nothing to click away from. */
@media (min-width: 92rem) {
  .locker-backdrop.is-open {
    opacity: 0;
    pointer-events: none;
  }
}

/* Legal pages ------------------------------------------------------------- */

/* Long-form prose in the same glass panel as the composer. Left-aligned and a
   little wider than the form, which is all these pages need. */
.legal {
  text-align: left;
  font-size: .92rem;
  line-height: 1.65;
  color: var(--sand);
}

.legal h2 {
  margin: 1.8rem 0 .5rem;
  font-size: 1.15rem;
}

.legal h2:first-child { margin-top: 0; }

.legal p { margin: .6rem 0; }

.legal ul {
  margin: .6rem 0;
  padding-left: 1.2rem;
}

.legal li { margin: .35rem 0; }

.legal strong { color: var(--sand); font-weight: 600; }

.legal code {
  font-size: .85em;
  padding: .1em .35em;
  border-radius: 5px;
  background: rgba(3, 14, 28, .55);
  border: 1px solid var(--panel-line);
  color: var(--cyan-bright);
}

.legal a,
.foot a {
  color: var(--cyan);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.legal a:hover,
.foot a:hover { color: var(--cyan-bright); }

/* /twitch-verify (netlify/functions/twitch-verify.mjs): the one action on the
   page, in the paper-and-rope gold the cast button wears. */
.legal .twitch-verify-button {
  display: inline-block;
  width: auto;
  padding: .65rem 1.4rem;
  border-radius: 6px;
  background: linear-gradient(180deg, #f2c66f, #d9a043);
  color: #2a1c06;
  text-decoration: none;
}

.legal .twitch-verify-button:hover { color: #2a1c06; }
