/* Cargo Sort.

   The whole game is two rows of coloured rectangles, so the styling has one
   job: make a container read as a container at 24px tall, and make "this is
   finished" obvious from across the room. Everything else stays out of the
   way - a puzzle this repetitive gets tiring fast if the frame is busy. */

:root {
  --bg: #0a1420;
  --panel: #11202f;
  --panel-2: #16293b;
  --line: #24405a;
  --ink: #e8f0f8;
  --muted: #8ba3b9;
  --accent: #34d399;
  --warn: #f87171;

  /* Container heights drive the whole vertical rhythm: stacks, berths and the
     gaps between them are all derived from this one number. The base value is
     kept separate because a ship's hold draws its containers smaller than the
     yard does - that size difference is most of what stops a berth from
     reading as just another stack. */
  --base-box-h: clamp(22px, 4.4vw, 44px);
  --box-h: var(--base-box-h);
  --stack-w: clamp(52px, 13vw, 100px);
  /* One number for the gap between containers, used by every height sum as
     well as by the flex gaps. When the gap was hard-coded in four places the
     lane heights and the gaps could drift apart, and a lane whose reserved
     height did not match its contents is a lane whose containers look unevenly
     spaced. */
  --box-gap: 3px;
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
}

body {
  margin: 0;
  background:
    radial-gradient(1200px 700px at 50% -15%, #16324b 0%, transparent 60%),
    var(--bg);
  color: var(--ink);
  font: 15px/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-tap-highlight-color: transparent;
  overscroll-behavior: none;
  display: flex;
  flex-direction: column;
}

/* The daily chip. Deliberately the only element on the board that is not part
   of the puzzle: it is the invitation, and the streak number is the argument. */
.daily {
  align-self: center;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 14px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel);
  color: var(--ink);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: border-color .15s, background .15s;
}
.daily:hover { border-color: #375d80; background: var(--panel-2); }
.daily-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: #fbbf24;
  box-shadow: 0 0 0 0 rgba(251, 191, 36, .5);
  animation: nudge 2.4s ease-in-out infinite;
}
.daily.is-done { border-color: #2c6a52; }
.daily.is-done .daily-dot { background: var(--accent); animation: none; }
.daily-streak { color: var(--muted); font-variant-numeric: tabular-nums; }
.daily.is-done .daily-streak { color: var(--accent); }
.daily.is-playing { border-color: var(--accent); background: rgba(52, 211, 153, .1); }

@keyframes nudge {
  0%, 100% { box-shadow: 0 0 0 0 rgba(251, 191, 36, .45); }
  50% { box-shadow: 0 0 0 6px rgba(251, 191, 36, 0); }
}

/* ---------------------------------------------------------------- top bar */

.topbar {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px clamp(12px, 3vw, 26px);
  border-bottom: 1px solid var(--line);
  background: rgba(10, 20, 32, .82);
  backdrop-filter: blur(8px);
  position: sticky;
  top: 0;
  z-index: 20;
}

.brand { display: flex; align-items: center; gap: 9px; margin-right: auto; }
.logo { width: 26px; height: 26px; color: #38bdf8; flex: none; }
.brand-name { font-weight: 650; letter-spacing: .2px; font-size: 16px; }

.hud { display: flex; gap: clamp(10px, 3vw, 26px); }
.stat { display: flex; flex-direction: column; align-items: flex-end; line-height: 1.15; }
.stat-label {
  font-size: 10px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
}
.stat b { font-size: 17px; font-variant-numeric: tabular-nums; white-space: nowrap; }

.tools { display: flex; gap: 6px; }
.icon-btn {
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--panel);
  color: var(--ink);
  font-size: 16px;
  cursor: pointer;
}
.icon-btn:hover:not(:disabled) { background: var(--panel-2); border-color: #375d80; }
.icon-btn:disabled { opacity: .35; cursor: default; }
.icon-btn.is-off { opacity: .45; }

/* The landing squash. `scale` is used rather than `transform` because the FLIP
   animation owns `transform` while a move is travelling - writing both to the
   same property would have one overwrite the other mid-flight. */
.box.landed { animation: land .19s cubic-bezier(.3, .9, .4, 1); }
@keyframes land {
  0% { scale: 1 .78; }
  55% { scale: 1 1.05; }
  100% { scale: 1 1; }
}

/* ----------------------------------------------------------------- board */

.board {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(14px, 3.5vh, 34px);
  padding: clamp(12px, 3vh, 28px) clamp(10px, 3vw, 26px) 0;
  max-width: 1200px;
  width: 100%;
  margin: 0 auto;
}

/* Berths. A manifest, not a container.

   Until the rule change a ship really did take single containers, so its berth
   was drawn as a lane - a column of empty slots, identical to the ones in the
   yard. When ships stopped taking single containers the drawing did not change,
   and the result was the worst kind of interface: the one place that looked
   like the obvious drop target was the one place that refused the drop. The
   berth is now a hull, a row of pips and a word, and it does not look like
   anywhere you could put a box. */
.berths {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: clamp(8px, 2vw, 18px);
  padding-bottom: 10px;
  /* A waterline, so the row reads as a harbour rather than as a toolbar. */
  border-bottom: 1px solid rgba(56, 189, 248, .16);
}

.berth {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  padding: 8px 11px 6px;
  border: 1px solid rgba(56, 189, 248, .22);
  border-radius: 10px;
  background: linear-gradient(180deg, transparent, rgba(255, 255, 255, .03));
  cursor: pointer;
  transition: border-color .15s, background .15s, opacity .2s;
}
.berth.is-full { border-color: #2c6a52; background: rgba(52, 211, 153, .07); }
.berth.shake { animation: shake .3s; border-color: var(--warn); }

/* Dragged over during a drag and refused. Muted rather than alarming: the
   player has not done anything wrong, they just have not finished the load. */
.berth.is-blocked {
  border-color: rgba(248, 113, 113, .55);
  background: rgba(248, 113, 113, .07);
  opacity: .82;
}

.berth .hull {
  width: 46px;
  height: 20px;
  background: var(--c);
  opacity: .42;
  clip-path: polygon(0 0, 100% 0, 86% 100%, 5% 100%);
  display: grid;
  place-items: center;
  transition: opacity .25s;
}
.berth .hull svg { width: 12px; height: 12px; fill: rgba(0, 0, 0, .55); }
.berth.is-full .hull { opacity: 1; }

.berth .swatch { font-size: 10px; color: var(--muted); white-space: nowrap; }
.berth.is-full .swatch { color: var(--accent); }

/* Two strengths of highlight, because they answer two different questions.
   "Where could this go" is asked once, when the load is lifted, and every legal
   lane answers it - so it stays quiet. "Where will this land if I let go" is
   asked continuously during a drag and gets the loud treatment. When both were
   the same colour the board turned into a field of green rectangles. */
.stack.is-target {
  border-color: rgba(52, 211, 153, .45);
  background: rgba(52, 211, 153, .06);
}
.stack.is-hover {
  border-color: var(--accent);
  background: rgba(52, 211, 153, .18);
  box-shadow: 0 0 0 2px rgba(52, 211, 153, .28);
}

/* The hint answer, which has to read as an instruction rather than as another
   thing to interpret - it appears unprompted, so it has to be the loudest
   thing on screen for the two seconds it lives. */
.stack.is-hint {
  border-color: #fbbf24;
  border-style: solid;
  background: rgba(251, 191, 36, .14);
  box-shadow: 0 0 0 2px rgba(251, 191, 36, .3);
  animation: hintPulse 1.1s ease-in-out infinite;
}
@keyframes hintPulse {
  0%, 100% { box-shadow: 0 0 0 2px rgba(251, 191, 36, .28); }
  50% { box-shadow: 0 0 0 7px rgba(251, 191, 36, .07); }
}

.stars {
  margin: 2px 0 8px;
  font-size: 24px;
  letter-spacing: 4px;
  color: #fbbf24;
  min-height: 28px;
}

.berth .swatch {
  display: flex;
  align-items: center;
  gap: 3px;
  font-size: 10px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* Yard. Wrapping flex rather than a grid, and that is deliberate: a grid keeps
   every row on the same column tracks, so a last row with fewer lanes than the
   first sits hard against the left edge while the row above it spans the full
   width. Measured on a 390px phone that put row one at x=19..304 and row two at
   x=19..299 - the same left edge, a shorter right edge, and a board that reads
   as crooked. Flex wraps each line and centres it independently, so every row
   is balanced against the one above it. */
.yard {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(8px, 2vw, 18px);
}

.stack {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column-reverse;
  justify-content: flex-start;
  width: var(--stack-w);
  height: calc(var(--box-h) * var(--cap) + (var(--cap) - 1) * var(--box-gap) + 4px);
  gap: var(--box-gap);
  padding: 2px;
  border-radius: 8px;
  /* A faint lane outline on every stack, not just the empty ones. Containers
     sit at the bottom of their lane, so without an outline the tops of the
     columns look ragged and there is no way to see where a container can
     actually go. */
  border: 1px solid rgba(140, 172, 205, .16);
  background: rgba(255, 255, 255, .015);
  cursor: pointer;
  /* Dragging has to win over the browser's own scroll gesture. The board
     already fits the viewport at every size we support - the playtest asserts
     that - so there is nothing to scroll away from here anyway. */
  touch-action: none;
  transition: border-color .15s, background .15s;
}

.stack:hover .box { filter: brightness(1.08); }

/* The load being carried, pinned to the pointer. */
.drag-ghost {
  position: fixed;
  z-index: 60;
  width: var(--stack-w);
  display: flex;
  flex-direction: column;
  gap: var(--box-gap);
  transform: translate(-50%, -85%);
  pointer-events: none;
  filter: drop-shadow(0 12px 18px rgba(0, 0, 0, .55));
}
.drag-ghost .box { opacity: .96; }
/* An empty stack is a legal destination, so it has to look like a place to
   put something rather than like nothing. */
.stack.is-empty { border-style: dashed; border-color: #2c4d6b; background: rgba(255, 255, 255, .025); }
.stack.is-source { border-color: var(--accent); background: rgba(52, 211, 153, .09); }
.stack.is-target { border-color: var(--accent); }
.stack.shake { animation: shake .3s; border-color: var(--warn); }

/* Containers. Colour plus a shape, because two of the seven colours sit close
   enough together that colour alone is not a fair read.

   The edge treatment matters more than it looks like it should. A vertical
   light-to-dark gradient per container makes every seam read as a thick dark
   band in the middle and a thin one at the corners, so a board of evenly spaced
   containers still looks like it has ragged gaps. A flat fill with a dark
   outline and a single light top rail gives each container an unambiguous
   silhouette, and the gap between two of them then reads the same everywhere. */
.box {
  position: relative;
  height: var(--box-h);
  border-radius: 4px;
  background-color: var(--c);
  background-image:
    repeating-linear-gradient(90deg, rgba(0, 0, 0, .16) 0 1.5px, transparent 1.5px 9px);
  border: 1px solid rgba(0, 0, 0, .38);
  border-top-color: rgba(255, 255, 255, .3);
  border-bottom-color: rgba(0, 0, 0, .5);
  display: grid;
  place-items: center;
  will-change: transform;
}
.box svg { width: 46%; height: 46%; fill: rgba(0, 0, 0, .42); }

/* Selection lifts with `top`, not `transform`, so the FLIP animation owns
   transform exclusively and the two never fight. */
.box.lifted { top: -7px; }

.yard.dragging .stack { cursor: grabbing; }

/* ------------------------------------------------------------------ type */

.hint {
  margin: 0;
  padding: 12px clamp(12px, 3vw, 26px) 18px;
  text-align: center;
  color: var(--muted);
  font-size: 13px;
  min-height: 42px;
}

/* ---------------------------------------------------------------- modals */

.modal {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: grid;
  place-items: center;
  padding: 18px;
  background: rgba(4, 10, 18, .74);
  backdrop-filter: blur(3px);
}
.modal[hidden] { display: none; }

.modal-card {
  position: relative;
  width: min(430px, 100%);
  padding: 22px 22px 18px;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: linear-gradient(180deg, var(--panel-2), var(--panel));
  box-shadow: 0 24px 60px rgba(0, 0, 0, .5);
  text-align: center;
}
.modal-card.narrow { width: min(340px, 100%); }
.modal-card h2 { margin: 4px 0 10px; font-size: 21px; }

.kicker {
  margin: 0;
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--accent);
}
.copy { margin: 0 0 6px; color: #bed2e4; font-size: 14px; }

.link-btn {
  position: absolute;
  top: 12px;
  right: 14px;
  border: 0;
  background: none;
  color: var(--muted);
  font-size: 13px;
  cursor: pointer;
}
.link-btn:hover { color: var(--ink); }

.modal-actions {
  display: flex;
  gap: 10px;
  justify-content: center;
  margin-top: 16px;
}
.primary, .ghost-btn {
  padding: 10px 20px;
  border-radius: 10px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  border: 1px solid transparent;
}
.primary { background: var(--accent); color: #05231a; }
.primary:hover { filter: brightness(1.07); }
.ghost-btn { background: transparent; border-color: var(--line); color: var(--ink); }
.ghost-btn:hover { background: var(--panel-2); }
.ghost-btn[hidden] { display: none; }

.dots { display: flex; gap: 6px; justify-content: center; margin-top: 14px; }
.dots i {
  width: 6px; height: 6px; border-radius: 50%;
  background: #2b4a66;
}
.dots i.on { background: var(--accent); }

/* Tutorial artwork: miniature versions of the real pieces. */
.art { display: grid; place-items: center; margin: 14px 0 2px; min-height: 96px; }
.art-stack { display: flex; flex-direction: column-reverse; gap: 3px; width: 62px; }
.art-berth { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.art-hull {
  width: 58px;
  height: 26px;
  background: #32d1a0;
  clip-path: polygon(0 0, 100% 0, 86% 100%, 5% 100%);
  display: grid;
  place-items: center;
}
.art-hull svg { width: 14px; height: 14px; fill: rgba(0, 0, 0, .5); }
.art-label { font-size: 11px; color: var(--muted); }
.art-box {
  height: 20px;
  border-radius: 4px;
  display: block;
}
.art-box { background: #3b82f6; box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .3); }
.art-box.c1 { background: #f59e0b; }
.art-box.c2 { background: #32d1a0; }
.art-box.lift { position: relative; top: -7px; }
.art-box.ghost { opacity: .35; }
.art-pair { display: flex; align-items: flex-end; gap: 12px; }
.art-arrow { color: var(--muted); font-size: 20px; padding-bottom: 12px; }

@keyframes shake {
  0%, 100% { translate: 0 0; }
  25% { translate: -5px 0; }
  75% { translate: 5px 0; }
}

.berth.pulse { animation: pulse .45s; }
@keyframes pulse {
  0% { box-shadow: 0 0 0 0 rgba(52, 211, 153, .55); }
  100% { box-shadow: 0 0 0 14px rgba(52, 211, 153, 0); }
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .001s !important; transition-duration: .001s !important; }
}

@media (max-width: 420px) {
  .brand-name { display: none; }
}

/* Phones get a tighter yard and a much shorter ship row. An empty berth is the
   largest thing on the board and it is the only part the player cannot
   interact with, so it earns the least space. */
@media (max-width: 560px) {
  :root {
    --base-box-h: clamp(22px, 6vw, 32px);
    --stack-w: clamp(52px, 17vw, 82px);
  }
  .berth {
    padding: 5px 7px 4px;
    gap: 4px;
  }
  .berth .hull { width: 34px; height: 7px; }
  .pips i { width: 7px; height: 7px; }
  .berth .swatch { font-size: 9px; }
  .board { gap: 10px; padding-top: 8px; }
  .berths { gap: 5px; }
  .yard { gap: 5px; }
  .stat b { font-size: 15px; }
  .icon-btn { width: 32px; height: 32px; }
}
