/* Color Flood — styles
 *
 * The same visual world as the sort game, deliberately: a bright board under
 * a summer sky, drawn with cartoon weight — thick ink outlines and hard,
 * unblurred drop shadows. The weight is doing a job here rather than being a
 * style. A flood board is a field of vivid colour edge to edge, so every
 * piece that is not board — buttons, cards, the blob's own outline — has to
 * be able to hold its own against it.
 *
 * Deliberately single-theme, so there are no prefers-color-scheme blocks:
 * every colour below is painted explicitly. */

:root {
  /* the scene */
  --sky-top:   #79cbf2;
  --sky-mid:   #a5e2fa;
  --sky-low:   #d3f1fc;

  /* surfaces and ink */
  --paper:     #fffdf7;
  --paper-warm:#fff3e0;
  --ink:       #2b2142;
  --ink-soft:  #6f6389;
  --ink-faint: rgba(43, 33, 66, .16);

  /* action */
  --accent:      #ff5d8f;
  --accent-deep: #e0446f;
  --gold:        #ffc93c;
  --grass:       #35c46b;
  --danger:      #d8365f;

  --lift:   4px;                       /* hard-shadow offset on raised things */
  --stroke: 3px;
  --radius: 16px;
}

*, *::before, *::after { box-sizing: border-box; }

/* Nothing here is text anybody wants to select, and a long-press on a phone
   selecting the word under the finger — or a drag across the board painting
   half the cells blue — is only ever an accident. Turned off everywhere, then
   turned back on for nothing, because there is nothing to copy. */
* {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}

html, body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
  touch-action: manipulation;
  /* Every size in this game is in px, vh or em off a px root, and the board
     is measured in JavaScript against the room it actually has. A browser
     that inflates the TEXT without telling the layout — which is what iOS
     does when it decides a column is too narrow to read — breaks that
     arithmetic: the swatch letters grow, the buttons grow, the board does
     not, and they end up on top of each other.
     
     100% rather than none, so pinch-zoom and the browser's own page zoom
     still scale everything together. What is refused is text being scaled on
     its own. The matching switch on Android is in MainActivity.java, because
     that one is a WebView setting rather than a CSS property. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  /* An explicit colour underneath the gradient: the gradient is a background
     IMAGE, so without this the computed background-color stays transparent. */
  background-color: var(--sky-mid);
  background-image:
    linear-gradient(180deg, var(--sky-top) 0%, var(--sky-mid) 42%, var(--sky-low) 100%);
  background-attachment: fixed;
  color: var(--ink);
  font: 16px/1.55 "Nunito", ui-rounded, "Segoe UI", system-ui, sans-serif;
  font-weight: 600;
  -webkit-font-smoothing: antialiased;
}

.app {
  max-width: 1040px;
  margin: 0 auto;
  /* --vvh is the visual viewport, published by app.ts. Preferred over dvh
     because dvh resolves to the viewport with the browser's toolbars
     retracted — the tallest it could be — and on iOS Safari the toolbar is
     drawn over the page, so a board sized to dvh runs underneath it. */
  height: 100vh;
  height: 100dvh;
  height: var(--vvh, 100dvh);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding:
    calc(18px + env(safe-area-inset-top))
    calc(16px + env(safe-area-inset-right))
    /* --ad-h is the banner. It is 0 everywhere except a phone build with an
       ad on screen: the plugin draws the banner as a native view OVER the
       page rather than resizing it, so this is the only thing standing
       between the color swatches and having an ad sit on top of them. */
    calc(16px + env(safe-area-inset-bottom) + var(--ad-h, 0px))
    calc(16px + env(safe-area-inset-left));
}

/* The stand-in for the banner, drawn only under ?ads=preview. Sized and
   placed exactly where the native one lands — above the home indicator,
   centred, 320x50 — so what it shows about the layout is true. */
.ad-preview {
  position: fixed;
  left: 0;
  right: 0;
  bottom: env(safe-area-inset-bottom);
  height: 50px;
  display: grid;
  place-items: center;
  background: rgba(255, 255, 255, .55);
  border-top: 2px dashed rgba(43, 45, 66, .35);
  pointer-events: none;
}
/* .ad-preview sets display, and a class beats the [hidden] attribute's own
   rule, so hiding it needs saying explicitly. Caught by looking at a
   screenshot: --ad-h had gone to 0 and the board had grown back, and the box
   was still sitting there over the swatches. */
.ad-preview[hidden] { display: none; }
.ad-preview span {
  width: 320px;
  height: 50px;
  display: grid;
  place-items: center;
  background: repeating-linear-gradient(
    -45deg, rgba(43, 45, 66, .06) 0 8px, rgba(43, 45, 66, .12) 8px 16px);
  border: 2px dashed rgba(43, 45, 66, .5);
  border-radius: 4px;
  color: var(--ink-soft);
  font-size: 12px;
  font-weight: 800;
  letter-spacing: .04em;
  text-transform: uppercase;
}

.screen { display: none; flex: 1; min-height: 0; }
.screen.is-active {
  display: flex;
  flex-direction: column;
  animation: rise .34s cubic-bezier(.2, .9, .3, 1.2) both;
  /* Everything below is sized so that it fits, and measured at every size a
     phone actually is. This is the net under that: a window shorter than any
     of them scrolls rather than hiding the buttons. Clipping is strictly
     worse than scrolling — a scrollbar tells you there is more, a clipped
     screen just quietly has no Play button on it. Costs nothing when the
     content fits, which is the usual case. */
  overflow-y: auto;
  overscroll-behavior: contain;
  /* ...and a scroll box CLIPS. CSS will not let one axis scroll while the
     other overflows visibly, so overflow-y: auto quietly makes overflow-x
     auto too, and this box cuts everything off at its padding edge.

     .board-wrap reaches 8px outside that edge on purpose — the board is
     square and limited by the width, so the side padding is the only width
     left to give it. What landed outside the clip was the 3px the board wears
     as a border: measured on a 393px phone the board sat at 12.5–380.5 and
     the clip ran 16–377, so its left and right sides were cut away entirely
     while its top and bottom kept theirs. A board with an outline on two
     sides looks like a board running off the screen.

     Widening the padding box by the same 8px and pulling it back with a
     negative margin moves the clip and nothing else: every screen's content
     box is exactly where it was. */
  margin-inline: -8px;
  padding-inline: 8px;
}
@keyframes rise {
  from { opacity: 0; transform: translateY(14px) scale(.985); }
  to   { opacity: 1; transform: none; }
}

/* ───────── buttons ───────── */

.btn {
  appearance: none;
  border: var(--stroke) solid var(--ink);
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  font-family: "Baloo 2", "Nunito", sans-serif;
  font-weight: 700;
  font-size: 16px;
  line-height: 1.1;
  padding: 11px 18px;
  border-radius: 13px;
  cursor: pointer;
  box-shadow: 0 var(--lift) 0 var(--ink);
  transition: transform .09s ease, box-shadow .09s ease, background .14s ease;
}
.btn:hover:not(:disabled) { background: var(--paper-warm); }
/* Press the button physically into the page. */
.btn:active:not(:disabled) { transform: translateY(var(--lift)); box-shadow: 0 0 0 var(--ink); }
.btn:disabled { opacity: .45; cursor: default; box-shadow: 0 var(--lift) 0 var(--ink-faint); }

.btn:focus-visible,
.choice:focus-visible,
.swatch:focus-visible,
input:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

.btn--primary { background: var(--accent); color: #fff; }
.btn--primary:hover:not(:disabled) { background: var(--accent-deep); }
.btn--ghost {
  background: rgba(255, 255, 255, .62);
  box-shadow: 0 var(--lift) 0 var(--ink-faint);
}
.btn--ghost:hover:not(:disabled) { background: #fff; }
.btn--quiet { font-size: 15px; color: var(--ink-soft); }
.btn--wide  { width: 100%; }

/* ───────── home ───────── */

/* Sized against the SHORTER of the two axes. A title at 9vw is fine on a tall
   phone and absurd on the same phone turned sideways, where there is width to
   spare and no height at all — so every step of the lockup takes min() of a
   width and a height, and the whole thing shrinks when the window is short
   rather than running off the bottom. */
.masthead { text-align: center; margin: clamp(2px, 1.2vh, 10px) 0 clamp(12px, 3.4vh, 26px); }

.masthead__mark {
  display: flex;
  justify-content: center;
  gap: 9px;
  padding-top: clamp(0px, 1.4vh, 10px);
  margin-bottom: clamp(6px, 1.8vh, 14px);
}
.masthead__mark span {
  width: clamp(18px, 3vh, 26px); height: clamp(18px, 3vh, 26px);
  border-radius: 7px;
  background: var(--c);
  border: var(--stroke) solid var(--ink);
  animation: bob 2.4s ease-in-out infinite;
}
.masthead__mark span:nth-child(2) { animation-delay: .18s; }
.masthead__mark span:nth-child(3) { animation-delay: .36s; }
@keyframes bob { 50% { transform: translateY(-8px) rotate(-4deg); } }

.masthead h1 {
  margin: 0;
  font-family: "Baloo 2", sans-serif;
  font-weight: 800;
  font-size: clamp(42px, min(9vw, 7.4vh), 74px);
  line-height: .98;
  letter-spacing: -.01em;
  color: #fff;
  /* Stroke behind the fill, then a hard drop below: cartoon lettering.
     
     Both in em, which is the whole point. They used to be sized off the
     viewport — the stroke off min(1vw, .9vh) and the drop off .9vh alone —
     while the type is sized off min(9vw, 7.4vh). Three different rulers for
     one piece of lettering, and on a tall narrow phone they came apart: at
     390px wide the type landed at 35px with a 7px drop under it, a fifth of
     the font. The shadow stopped reading as a shadow and started filling the
     counters of the o's, so the word looked double-decked.
     
     In em they cannot drift, because there is only one ruler. .167 for both
     is the sort game's, worked back from its rendered title: 7px of stroke
     and 7px of drop on 42px type. It had looked thinner here only because
     this title was rendering at 35px — the sort game's smaller ladder sits
     behind a max-height: 720px query that a 852-tall phone never reaches, so
     its home screen is at the full 42px, and the floor below matches it. */
  -webkit-text-stroke: .167em var(--ink);
  paint-order: stroke fill;
  text-shadow: 0 .167em 0 var(--ink);
  text-wrap: balance;
}
.masthead__tag {
  margin: clamp(8px, 2vh, 16px) auto 0;
  max-width: 30ch;
  color: var(--ink);
  font-weight: 700;
  font-size: clamp(14px, 1.9vh, 17px);
  text-wrap: balance;
}

/* The menu fills the screen rather than stacking at the top of it.
   
   Content-sized cards left 188px of nothing under the buttons on a 393x852
   phone and 264px on a 430x932 one — the whole menu bunched into the top two
   thirds with the game's name floating above it. The four rows now take a
   share of the height instead: 11.5vh each, which comes out at 98px on a
   852-tall phone, the same as the sort game's.
   
   Capped at both ends, because a row that is a share of the height is a row
   that can become silly. 66px is a comfortable tap target with the sub-line
   still on it; 112px stops a tablet turning four buttons into four billboards.
   
   The gap is 1.9vh and the rows 11.3vh — 16px and 96px on that phone — and
   the block is centred in whatever room is left rather than stretched across
   it. Both numbers were measured off a screenshot of the sort game's own home
   screen, border to border: 96px cards on a 112px pitch. Letting the leftover
   fall into the gaps instead (space-evenly) put 52px between the cards, more
   than three times the sort game's, and it read as four separate things
   rather than one menu. */
.choices {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-rows: repeat(4, clamp(66px, 11.3vh, 110px));
  align-content: center;
  gap: clamp(9px, 1.9vh, 20px);
  width: 100%;
  max-width: 520px;
  margin: 0 auto;
}

.choice {
  display: flex;
  align-items: center;
  gap: clamp(10px, 2vw, 16px);
  width: 100%;
  padding: clamp(9px, 1.7vh, 16px) clamp(12px, 3vw, 18px);
  text-align: left;
  background: var(--paper);
  border: var(--stroke) solid var(--ink);
  border-radius: var(--radius);
  box-shadow: 0 6px 0 var(--ink);
  font: inherit;
  color: var(--ink);
  cursor: pointer;
  transition: transform .09s ease, box-shadow .09s ease, background .14s ease;
}
.choice:hover { background: var(--paper-warm); }
.choice:active { transform: translateY(4px); box-shadow: 0 2px 0 var(--ink); }
.choice__body { flex: 1; display: grid; gap: 2px; }
.choice__name {
  font-family: "Baloo 2", sans-serif;
  font-weight: 800;
  font-size: clamp(17px, 2.5vh, 22px);
  line-height: 1.1;
}
.choice__sub { color: var(--ink-soft); font-size: clamp(12px, 1.7vh, 14px); }
.choice__go { font-size: 30px; color: var(--ink-soft); line-height: 1; }

/* a four-band flood, shrunk to an icon */
.choice__flood {
  flex: 0 0 clamp(32px, 5.5vh, 46px);
  height: clamp(32px, 5.5vh, 46px);
  display: grid;
  grid-template: repeat(2, 1fr) / repeat(2, 1fr);
  gap: 3px;
}
.choice__flood i {
  border: 2px solid var(--ink);
  border-radius: 5px;
}
.choice__flood i:nth-child(1) { background: #f5423c; }
.choice__flood i:nth-child(2) { background: #ffd028; }
.choice__flood i:nth-child(3) { background: #3b7bf7; }
.choice__flood i:nth-child(4) { background: #2fc15e; }

/* two corners of a board, lit */
.choice__merge {
  flex: 0 0 clamp(32px, 5.5vh, 46px);
  height: clamp(32px, 5.5vh, 46px);
  position: relative;
  border: var(--stroke) solid var(--ink);
  border-radius: 9px;
  background: var(--paper-warm);
}
.choice__merge i {
  position: absolute;
  width: 40%;
  height: 40%;
  border: 2px solid var(--ink);
  border-radius: 4px;
}
.choice__merge i:nth-child(1) { left: 4px; bottom: 4px; background: #f5423c; }
.choice__merge i:nth-child(2) { right: 4px; top: 4px; background: #3b7bf7; }

/* a calendar page, borrowed wholesale from the sort game */
.choice__cal {
  flex: 0 0 clamp(32px, 5.5vh, 46px);
  height: clamp(32px, 5.5vh, 46px);
  position: relative;
  border: var(--stroke) solid var(--ink);
  border-radius: 9px;
  background: var(--paper-warm);
  display: grid;
  grid-template: repeat(2, 1fr) / repeat(2, 1fr);
  gap: 4px;
  padding: 13px 7px 7px;
}
.choice__cal-top {
  position: absolute;
  inset: 0 0 auto;
  height: 9px;
  background: var(--accent);
  border-bottom: 2px solid var(--ink);
  border-radius: 5px 5px 0 0;
}
.choice__cal-dot { border-radius: 2px; background: var(--ink-faint); }
.choice__cal-dot.is-on { background: var(--gold); }

.menu-foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin-top: clamp(12px, 3vh, 30px);
}

/* ───────── sub screens ───────── */

.subhead {
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
  max-width: 620px;
  margin: 0 auto clamp(10px, 2.4vh, 22px);
}
.subhead__title { flex: 1; text-align: center; line-height: 1.15; }
.subhead__title h2 {
  margin: 0;
  font-family: "Baloo 2", sans-serif;
  font-weight: 800;
  font-size: 27px;
}
.subhead__title span { color: var(--ink-soft); font-size: 14px; font-weight: 700; }
/* keeps the heading centred against the back button */
.subhead__balance { flex: 0 0 96px; }
/* ...but only where centring is affordable. The reservation costs 96px of
   empty space, and a phone does not have it to give: measured, a 375px screen
   left the title column 116px, and "Daily Puzzle" is 153px at 27px type, so
   the heading wrapped onto two lines, took "Today: Merge · Easy" under it as
   two more, and at 320px broke the back button itself into "←" over "Home".
   Below 460px the reservation is worth less than the lines it costs: drop it,
   take the heading down five points, and both fit on one line at 320px with
   room over. The heading then sits right of true centre by about the width of
   the button, which is what Color Match does on the same phones and reads as
   centred next to a left-hand button. */
@media (max-width: 460px) {
  .subhead__balance { flex-basis: 0; }
  .subhead__title h2 { font-size: 22px; }
}

.card {
  background: var(--paper);
  border: var(--stroke) solid var(--ink);
  border-radius: var(--radius);
  padding: clamp(13px, 2.4vh, 22px);
  box-shadow: 0 6px 0 var(--ink);
}
/* width: 100% matters: this is a flex item in a column screen with auto side
   margins, and auto side margins make a flex item shrink to its content — so
   without it the card resizes when its text does. */
.card--single { width: 100%; max-width: 420px; margin: 0 auto; }
.card__title {
  margin: 0 0 4px;
  font-family: "Baloo 2", sans-serif;
  font-weight: 800;
  font-size: 25px;
  line-height: 1.1;
}
.card__body { margin: 0 0 16px; color: var(--ink-soft); font-size: 15px; }
.card__note { margin: 14px 0; color: var(--ink-soft); font-size: 14px; min-height: 1.2em; }

/* Two games, side by side above the slider. Buttons rather than a second
   slider: a slider says "more of the same in this direction" and these two
   are not on one axis. */
.difficulty--modes {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 9px;
  margin-bottom: 18px;
}
.diff {
  padding: 11px 6px;
  border-radius: 12px;
  border: var(--stroke) solid var(--ink);
  background: var(--paper-warm);
  color: var(--ink-soft);
  font: inherit;
  font-family: "Baloo 2", sans-serif;
  font-weight: 700;
  font-size: 16px;
  cursor: pointer;
  box-shadow: 0 var(--lift) 0 var(--ink);
  transition: transform .09s ease, box-shadow .09s ease, background .14s ease;
}
.diff:hover { background: #ffe8c6; }
.diff:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
/* The chosen one is pressed INTO the card, which is the same language the
   buttons use when you hold them down. */
.diff.is-on {
  background: var(--gold);
  color: var(--ink);
  transform: translateY(var(--lift));
  box-shadow: 0 0 0 var(--ink);
}

/* ───────── the difficulty slider ─────────
   Five settings, and a slider rather than five buttons: it is one row
   whatever the count, it names the setting at full size, and it is the one
   control here whose meaning IS an ordering. */
.setting { margin-bottom: 4px; }
.setting__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 6px;
}
.setting__label {
  font-size: 13px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.setting__name {
  font-family: "Baloo 2", sans-serif;
  font-size: 20px;
  color: var(--ink);
}
.setting__slider {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 26px;
  margin: 0;
  background: transparent;
  cursor: pointer;
}
.setting__slider::-webkit-slider-runnable-track {
  height: 10px;
  border-radius: 999px;
  border: var(--stroke) solid var(--ink);
  background: var(--paper-warm);
}
.setting__slider::-moz-range-track {
  height: 10px;
  border-radius: 999px;
  border: var(--stroke) solid var(--ink);
  background: var(--paper-warm);
}
/* The thumb carries the same gold and the same hard shadow as a selected
   button would, so it reads as part of the same set of pieces. */
.setting__slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 26px;
  height: 26px;
  margin-top: -10px;
  border-radius: 50%;
  border: var(--stroke) solid var(--ink);
  background: var(--gold);
  box-shadow: 0 2px 0 var(--ink);
  transition: transform .09s ease;
}
.setting__slider::-moz-range-thumb {
  width: 26px;
  height: 26px;
  border: var(--stroke) solid var(--ink);
  border-radius: 50%;
  background: var(--gold);
  box-shadow: 0 2px 0 var(--ink);
}
.setting__slider:active::-webkit-slider-thumb { transform: scale(1.08); }
.setting__slider:focus-visible { outline: 3px solid var(--accent); outline-offset: 4px; border-radius: 999px; }

.setting__ticks { display: flex; justify-content: space-between; padding: 0 4px; margin-top: 2px; }
.setting__ticks span { font-size: 11px; color: var(--ink-soft); flex: 1; text-align: center; }
.setting__ticks span:first-child { text-align: left; }
.setting__ticks span:last-child { text-align: right; }
.setting__ticks span.is-on { color: var(--ink); font-weight: 700; }

.difficulty__blurb {
  margin: 12px 0 4px;
  color: var(--ink-soft);
  font-size: 14px;
  /* Two lines' worth, always, so picking a setting whose description happens
     to fit on one line does not shorten the card and jump everything under
     it up the screen. */
  line-height: 1.5;
  min-height: 3em;
}

.streak { display: flex; gap: 10px; justify-content: center; margin: clamp(7px, 1.7vh, 14px) 0 0; }

/* ───────── the level grid ───────── */

#screen-levels.is-active { overflow: hidden; }
.level-grid {
  list-style: none;
  margin: 0;
  padding: 0 2px 8px;
  display: grid;
  /* Adapt to whatever room the window gives — a phone gets a tight five, a
     desktop a dozen. align-content stops the rows drifting apart to fill the
     height when there are only a few of them. */
  grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
  gap: 9px;
  align-content: start;
  /* The one place scrolling is welcome: a hundred tiles is deliberately
     taller than the window. Everything else fits. */
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* ...and a scroll box CLIPS. CSS will not let one axis scroll while the
     other overflows visibly, so overflow-y: auto quietly makes overflow-x
     auto too, and this box cuts everything off at its padding edge.

     .board-wrap reaches 8px outside that edge on purpose — the board is
     square and limited by the width, so the side padding is the only width
     left to give it. What landed outside the clip was the 3px the board wears
     as a border: measured on a 393px phone the board sat at 12.5–380.5 and
     the clip ran 16–377, so its left and right sides were cut away entirely
     while its top and bottom kept theirs. A board with an outline on two
     sides looks like a board running off the screen.

     Widening the padding box by the same 8px and pulling it back with a
     negative margin moves the clip and nothing else: every screen's content
     box is exactly where it was. */
  margin-inline: -8px;
  padding-inline: 8px;
}

.tile {
  width: 100%;
  /* Slightly wider than tall, so five rows fit a phone without scrolling
     past the first block. */
  aspect-ratio: 1 / 0.82;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  border: var(--stroke) solid var(--ink);
  border-radius: 12px;
  background: var(--paper-warm);
  color: var(--ink);
  font: inherit;
  font-family: "Baloo 2", sans-serif;
  font-weight: 800;
  font-size: 19px;
  cursor: pointer;
  box-shadow: 0 var(--lift) 0 var(--ink);
  transition: transform .09s ease, box-shadow .09s ease, background .14s ease;
}
.tile:hover:not(:disabled) { background: #ffe8c6; }
.tile:active:not(:disabled) { transform: translateY(var(--lift)); box-shadow: 0 0 0 var(--ink); }
.tile:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
/* Done, and done at par. Two states worth telling apart, because par is the
   thing worth going back for. */
.tile.is-done { background: var(--grass, #35c46b); color: #fff; }
.tile.is-par { background: var(--gold); color: var(--ink); }
.tile:disabled {
  opacity: .5;
  cursor: default;
  box-shadow: 0 var(--lift) 0 var(--ink-faint);
  background: rgba(255, 255, 255, .5);
}
.tile__mark { font-size: 11px; font-weight: 800; letter-spacing: .06em; opacity: .85; }
/* A locked level has nothing to report, so it says nothing rather than
   showing an empty space where a score would be. */
.tile:disabled .tile__mark { visibility: hidden; }

/* ───────── the daily calendar ───────── */

.cal__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: clamp(5px, 1.2vh, 10px);
}
.cal__head h3 {
  margin: 0;
  flex: 1;
  text-align: center;
  font-family: "Baloo 2", sans-serif;
  font-weight: 800;
  font-size: clamp(17px, 2.5vh, 21px);
}
.cal__head .btn { padding: 5px 13px; font-size: 21px; line-height: 1; }

.cal__names, .cal {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: clamp(3px, .8vh, 6px);
  margin: 0;
  padding: 0;
  list-style: none;
}
.cal__names {
  margin-bottom: 6px;
  color: var(--ink-soft);
  font-weight: 800;
  font-size: 13px;
  text-align: center;
}

.cal__day {
  position: relative;
  width: 100%;
  aspect-ratio: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  border: 2px solid var(--ink);
  border-radius: 10px;
  background: var(--paper-warm);
  font-family: "Baloo 2", sans-serif;
  font-weight: 800;
  font-size: clamp(12px, 1.9vh, 15px);
  color: var(--ink);
  cursor: pointer;
  padding: 0;
}
.cal__day:hover:not(:disabled) { background: #fff; }
.cal__day:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
/* A day that cannot be played is dashed rather than merely faint: before the
   first daily it is not a puzzle that was missed, it is a day the game did
   not exist, and after today it has not happened yet. */
.cal__day:disabled { opacity: .38; cursor: default; border-style: dashed; }
.cal__day.is-today { box-shadow: 0 0 0 3px var(--gold); }
.cal__day.is-done { background: var(--grass); color: #fff; }
.cal__day.is-done .cal__dot { border-color: #fff; }
/* The blanks before the first of the month keep the grid aligned without
   being anything a reader or a screen reader has to account for. */
.cal--blank { visibility: hidden; }

.cal__dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border: 2px solid var(--ink);
  border-radius: 50%;
}
.cal__dot--flood { background: #3b7bf7; }
.cal__dot--merge { background: var(--accent); }

.cal__legend {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin: clamp(6px, 1.5vh, 12px) 0 0;
  color: var(--ink-soft);
  font-size: 13.5px;
  font-weight: 700;
}
.cal__legend .cal__dot { margin-left: 8px; }
.cal__legend .cal__dot:first-child { margin-left: 0; }

/* ───────── the game ───────── */

.topbar {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  max-width: 620px;
  margin: 0 auto 14px;
}
.topbar__title { flex: 1; text-align: center; min-width: 0; }
.topbar__title h2 {
  margin: 0;
  font-family: "Baloo 2", sans-serif;
  font-weight: 800;
  font-size: 22px;
  line-height: 1.1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The setting, the size and par, on their own full-width line.
   
   They used to sit under the heading inside the topbar, sharing the row with
   the Menu button and two stat pills — which left the title column 88px wide
   on a 390px phone. "Extra Hard · 14×14 · par 13" does not fit in 88px, so it
   wrapped, and it wrapped after a separator: a line reading "· par 12" on its
   own. Out here it has the full width and needs one line. */
.subline {
  margin: 0 auto 10px;
  max-width: 100%;
  text-align: center;
  color: var(--ink-soft);
  font-size: 13px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.subline:empty { display: none; }

.topbar__stats { display: flex; gap: 8px; }
.stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  line-height: 1.05;
  min-width: 52px;
  padding: 5px 8px;
  border-radius: 11px;
  border: 2px solid var(--ink);
  background: var(--paper-warm);
}
.stat b {
  font-family: "Baloo 2", sans-serif;
  font-weight: 800;
  font-size: 21px;
  font-variant-numeric: tabular-nums;
}
.stat.is-out b { color: var(--accent-deep); }
.stat small {
  color: var(--ink-soft);
  font-size: 10px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .07em;
}

/* The board takes whatever room is left after the chrome, and is square. Both
   halves matter: min-height: 0 lets a flex item shrink below its content, and
   without it a 15×15 board pushes the colour picker off a short window. */
/* The one line of teaching on the drawn levels. Empty on every other board,
   and it takes no room when empty. */
.brief {
  margin: 0 auto 12px;
  max-width: 46ch;
  text-align: center;
  font-size: 14.5px;
  font-weight: 700;
  color: var(--ink);
  background: rgba(255, 255, 255, .62);
  border-radius: 12px;
  padding: 9px 14px;
}
.brief:empty { display: none; }

.board-wrap {
  flex: 1;
  min-height: 0;
  display: grid;
  /* A square board on a tall phone cannot use all the height it is given —
     at 390x844 there are spare pixels whatever is done, because the board is
     limited by the width. The only question is where they go, and the answer
     is: shared out, not pooled. Bottom-aligning the board was tried and put
     the whole surplus in one band under the heading, which looked worse than
     the two it replaced. So it stays centred and the gaps below grow with the
     window instead, until what is left over reads as spacing rather than as a
     hole. */
  place-items: center;
  margin-bottom: 14px;
  /* The board reaches past the app's side padding.
     
     It is square, so on a phone it is limited by the WIDTH and nothing else —
     a 390px screen with 16px padding a side left a 14x14 board at 350px and
     135px of unusable height above and below it. Height cannot help a square
     board, so the only way to make the squares bigger is to give it more
     width, and the padding was the only width left to give. Every other
     screen keeps its comfortable margins; this is the one that is all board. */
  margin-inline: -8px;
}
.board {
  display: grid;
  /* The ink shows through as a hairline between cells, which is the whole
     reason the board's own background is ink rather than nothing. Bands read
     as bands either way, but the seams make the cells countable — and
     counting cells is how you check a move before spending it. */
  gap: 1px;
  border: var(--stroke) solid var(--ink);
  border-radius: 12px;
  box-shadow: 0 6px 0 var(--ink);
  overflow: hidden;
  /* Sized in JS to the largest square that fits, in whole pixels per cell —
     fractional cell sizes leave hairline seams between the cells on some
     zoom levels, and a flood board is nothing but seams. */
  background: var(--ink);
  touch-action: manipulation;
}

.cell {
  /* A button, so every cell is a real target with real focus and press
     behaviour — but with every default a button brings stripped off, or the
     browser's own border draws over the seams above. */
  appearance: none;
  border: 0;
  margin: 0;
  padding: 0;
  position: relative;
  display: grid;
  place-items: center;
  font-family: "Baloo 2", sans-serif;
  font-weight: 800;
  line-height: 1;
  user-select: none;
  /* The colour change is the whole animation. Everything else about a move —
     the outline moving, the letters changing — happens at the same time and
     over the same duration, so a move reads as one event. */
  transition: background-color .22s ease;
}
/* The mark is there so the board is readable without relying on colour alone.
   It is deliberately faint: on a solved board it is the only thing left to
   look at, and a grid of hard black letters is not a reward. */
/* Legible, not decorative. This letter is the whole of the colour blind
   assist, and at the half-opacity it used to wear it was ornament — readable
   if you already knew what it said. */
.cell span { opacity: .9; pointer-events: none; }
.board.no-marks .cell span { display: none; }

/* Cells that just changed hands. A short pop, not a long one: at Expert a
   single move can turn forty cells over, and anything slower than this reads
   as lag rather than as feedback. */
@keyframes gained { from { transform: scale(.55); opacity: .35; } }
.cell.is-new { animation: gained .26s cubic-bezier(.2, .9, .3, 1.6) both; }

/* The blob's outline, drawn as inset shadows on the cells at its edge rather
   than as borders — a border would change the cell's size and shuffle the
   whole grid every move. The colour is the same ink as everything else, so
   the blob reads as a held piece rather than as a highlight. */
.cell { box-shadow: none; }

/* Where the blob starts. A dot in the corner rather than a ring around the
   middle: the middle already has the cell's letter in it, and two marks on
   top of each other read as neither. */
.origin::after {
  content: "";
  position: absolute;
  left: 13%;
  top: 13%;
  width: 22%;
  height: 22%;
  border-radius: 50%;
  background: rgba(43, 33, 66, .55);
  pointer-events: none;
}

/* ───────── the colour picker ───────── */

/* The colours share one row, always.
   
   They used to be 54px each and allowed to wrap, which on a 390px phone put
   six of them on THREE rows — 118px of height, taken straight out of the
   board, which is the thing anybody is actually looking at. Sharing the row
   instead means they shrink to fit rather than falling over: six across a
   358px screen come out 51px each. The cap stops two colours on a teaching
   level stretching into paddles. */
.picker {
  display: flex;
  justify-content: center;
  flex-wrap: nowrap;
  gap: 10px;
  /* width: 100% is load-bearing, and for the same reason it is on the cards.
     This is a flex item in a column, and auto side margins make a flex item
     shrink to fit its CONTENT rather than fill the line. Without it the row
     was 274px wide inside a 358px screen, and since the swatches size
     themselves from the row, they came out 37px instead of 51px — the fix for
     the wrapping made them smaller instead. */
  width: 100%;
  margin: 0 auto;
  max-width: 620px;
}
.swatch {
  flex: 1 1 0;
  max-width: 54px;
  aspect-ratio: 1;
  border: var(--stroke) solid var(--ink);
  border-radius: 14px;
  box-shadow: 0 var(--lift) 0 var(--ink);
  cursor: pointer;
  font: inherit;
  font-family: "Baloo 2", sans-serif;
  font-weight: 800;
  font-size: 19px;
  display: grid;
  place-items: center;
  transition: transform .09s ease, box-shadow .09s ease, opacity .14s ease;
}
.swatch:active:not(:disabled) { transform: translateY(var(--lift)); box-shadow: 0 0 0 var(--ink); }
/* The colour the blob already is. Not hidden — where a colour sits in the row
   has to stay put, or the player reaches for the wrong one — but clearly not
   a move. */
.swatch:disabled {
  cursor: default;
  opacity: .4;
  transform: translateY(var(--lift));
  box-shadow: 0 0 0 var(--ink-faint);
}
.swatch span { opacity: .85; }
/* The picker needs the letters for the same reason the board does, so the one
   switch governs both. */
.picker.no-marks .swatch span { display: none; }

/* The colour a hint just named. It pulses rather than simply lighting up,
   because the row is already six saturated squares and a static highlight on
   one of them is not reliably the first thing the eye finds. Stops as soon as
   a move is played. */
.swatch.is-hinted {
  animation: hinted 1.1s ease-in-out infinite;
}
@keyframes hinted {
  0%, 100% { box-shadow: 0 var(--lift) 0 var(--ink), 0 0 0 0 var(--accent); }
  50%      { box-shadow: 0 var(--lift) 0 var(--ink), 0 0 0 7px var(--accent); }
}
@media (prefers-reduced-motion: reduce) {
  .swatch.is-hinted {
    animation: none;
    box-shadow: 0 var(--lift) 0 var(--ink), 0 0 0 5px var(--accent);
  }
}

.toolbar {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin-top: 16px;
}

.status {
  text-align: center;
  margin: 12px auto 0;
  max-width: 52ch;
  /* Room for two lines always, so a message appearing cannot push the board
     around or reintroduce a scrollbar. */
  min-height: 2.7em;
  font-size: 15px;
  font-weight: 700;
  color: var(--ink-soft);
}
.status.is-good { color: #1f8f4d; }
.status.is-warn { color: var(--accent-deep); }

.keys {
  text-align: center;
  margin: 6px 0 0;
  font-size: 12.5px;
  color: rgba(43, 33, 66, .5);
}
.keys b { color: var(--ink); font-weight: 800; }
/* No keyboard to speak of, so the hint is only noise. */
@media (pointer: coarse) { .keys { display: none; } }

/* ───────── overlays ───────── */

.overlay {
  position: fixed;
  inset: 0;
  background: rgba(43, 33, 66, .5);
  display: grid;
  place-items: center;
  padding:
    calc(20px + env(safe-area-inset-top))
    calc(20px + env(safe-area-inset-right))
    calc(20px + env(safe-area-inset-bottom))
    calc(20px + env(safe-area-inset-left));
  z-index: 30;
  animation: fade .18s ease both;
}
.overlay[hidden] { display: none; }
@keyframes fade { from { opacity: 0; } }

.modal {
  background: var(--paper);
  border: var(--stroke) solid var(--ink);
  border-radius: 22px;
  padding: 28px;
  max-width: 390px;
  width: 100%;
  max-height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  text-align: center;
  box-shadow: 0 8px 0 var(--ink);
  animation: pop .34s cubic-bezier(.2, .9, .3, 1.5) both;
}
.modal--wide { max-width: 540px; text-align: left; }
@keyframes pop { from { opacity: 0; transform: scale(.86) translateY(14px); } }

.modal h2 {
  margin: 0 0 6px;
  font-family: "Baloo 2", sans-serif;
  font-weight: 800;
  font-size: 30px;
  line-height: 1.05;
}
.modal__line { color: var(--ink-soft); font-size: 15px; margin: 12px 0 0; }
.modal__swatch {
  width: 74px; height: 74px;
  margin: 0 auto 16px;
  border-radius: 20px;
  border: var(--stroke) solid var(--ink);
  box-shadow: 0 5px 0 var(--ink);
}
.modal__actions {
  display: flex;
  gap: 10px;
  justify-content: center;
  margin-top: 22px;
  flex-wrap: wrap;
}
/* Says what just happened, under the buttons that did it. Empty until then,
   so it takes no room and cannot be read as a warning already there. */
.modal__note { margin: 14px 0 0; min-height: 0; font-size: 14px; color: var(--ink-soft); }
.modal__note:empty { margin: 0; }

.howto { margin: 14px 0 0; padding-left: 22px; color: var(--ink-soft); font-size: 15px; }
.howto li { margin-bottom: 10px; }
.howto b { color: var(--ink); }

.settings-list { display: grid; gap: 12px; margin-top: 20px; text-align: left; }
/* Each row is a card in its own right — the same paper, ink and weight as
   everything else on the screen, rather than a hairline rule pretending to be
   a list. It is what the sort game's settings look like, and the two should
   not need explaining to each other. */
.settings-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 16px;
  background: var(--paper-warm);
  border: var(--stroke) solid var(--ink);
  border-radius: 15px;
}
/* The one row that destroys something wears its own colour, and says so twice:
   the label is red and so is the border. */
.settings-row--danger { border-color: var(--danger); }
.settings-row--danger > span { color: var(--danger); }
.settings-row > span {
  font-family: "Baloo 2", sans-serif;
  font-weight: 800;
  font-size: 17px;
  line-height: 1.15;
}
.settings-row .btn { padding: 8px 16px; font-size: 16px; flex: none; }
.settings-row .btn[aria-pressed="true"] { background: var(--gold); }

/* A row whose choice needs a sentence to be a fair question. "Personalized
   ads" and "Send usage data" are both things somebody is agreeing to, and a
   two-word label with a switch beside it is not enough to agree to. The name
   and its note share the left column; the switch keeps the right. */
.settings-row--stack > div { min-width: 0; }
.settings-row--stack b {
  display: block;
  font-family: "Baloo 2", sans-serif;
  font-weight: 800;
  font-size: 17px;
  line-height: 1.15;
}
.settings-row__note {
  margin: 4px 0 0;
  color: var(--ink-soft);
  font-size: 13.5px;
  line-height: 1.35;
}

/* ───────── tall windows ─────────
   Where the surplus goes.

   The board is square, so on a phone it is limited by the width and a tall
   screen has height it cannot use — 121px spare at 390x844 even with the
   board at its widest. Spending that on the gaps is right, but ONLY here:
   the same spacing on a short window comes straight out of the board, which
   is height-limited there instead. Measured, growing the gaps continuously
   with vh cost a 700px-tall screen a whole step of cell size — the board
   quantises to whole pixels per cell, so it drops 14px at a time on a 14-wide
   board and there is no such thing as a small loss.

   Hence a threshold rather than a clamp: below it, every pixel goes to the
   board; above it, there are pixels to spare and they go to the spacing. */
@media (min-height: 780px) {
  .topbar { margin-bottom: 20px; }
  .board-wrap { margin-bottom: 24px; }
  .toolbar { margin-top: 26px; }
  .status { margin-top: 18px; }
  /* A bigger target, since there is room for one. */
  .swatch { max-width: 60px; }
}
@media (min-height: 900px) {
  .topbar { margin-bottom: 26px; }
  .board-wrap { margin-bottom: 30px; }
  .toolbar { margin-top: 32px; }
  .status { margin-top: 22px; }
  .swatch { max-width: 66px; }
}

/* ───────── short windows, on the game screen ─────────
   The mirror of the block above. Where a tall window has height the board
   cannot use, a short one has none to spare and the board is the thing
   starved: measured on a 320x568 phone, a 7x7 board came out 21px a cell
   with the width able to afford 43. Every pixel taken off the chrome here
   goes straight into the squares, so the gaps close up and the status line
   keeps its two reserved lines at a smaller size rather than losing them —
   a status that can change height would resize the board mid-game. */
@media (max-height: 700px) {
  /* The title comes down with the rest of it. A 42px floor is right on a
     phone held upright and far too much on a short window, which is exactly
     the ladder the sort game climbs at 720 and 600. */
  .masthead h1 { font-size: clamp(36px, min(9vw, 7.4vh), 56px); }
  .topbar { margin-bottom: 8px; }
  .subline { margin-bottom: 6px; }
  .board-wrap { margin-bottom: 8px; }
  .toolbar { margin-top: 10px; gap: 8px; }
  .toolbar .btn { padding: 9px 14px; }
  .status { font-size: 13.5px; min-height: 2.4em; margin-top: 8px; }
  .swatch { max-width: 48px; }
}

/* ───────── short windows ─────────
   Everything above scales with the height it is given, so this is only for
   what cannot be scaled: pieces that are decoration rather than information,
   which go entirely rather than shrinking to illegible. */
@media (max-height: 560px) {
  .masthead h1 { font-size: clamp(26px, min(8vw, 7vh), 40px); }
  .masthead__mark { display: none; }
  .status { min-height: 1.4em; margin-top: 8px; }
  .toolbar { margin-top: 10px; }
  .swatch { width: 44px; height: 44px; font-size: 17px; }
  .brief { padding: 6px 12px; font-size: 13px; margin-bottom: 8px; }
}
@media (max-height: 500px) {
  /* A phone on its side. Wide, and with almost no height at all — so the
     calendar has to be sized by the height it has rather than by the width
     it happens to be given, or six rows of square cells run off the bottom.
     Seven columns of (available height / 6) works out at about 54vh across,
     which is why that number and not a rounder one. */
  .cal__names, .cal { max-width: min(100%, 54vh); margin-inline: auto; }
}
@media (max-height: 460px) {
  /* A phone on its side has no height to share out, so the rows go back to
     being the size of what is in them and the menu scrolls if it must. A
     share of 390px is not a share worth having. */
  .choices { display: grid; grid-template-rows: none; grid-auto-rows: min-content;
             align-content: start; flex: 0 1 auto; }
  .masthead__tag { display: none; }
  .cal__legend { display: none; }
  .difficulty__blurb { min-height: 0; }
  .keys { display: none; }
  /* The names carry it on their own here — Flood, Merge, Daily, Random are
     not lines anybody has to read twice. */
  .choice__sub { display: none; }
  .choice { padding: 7px 12px; }
  .menu-foot { margin-top: 10px; }
  .setting__ticks { font-size: 10px; }
  .card__note { margin: 8px 0; }
  /* The three numbers still fit; they just stop shouting. */
  .streak .stat { min-width: 44px; padding: 2px 6px; }
  .streak .stat b { font-size: 17px; }
  .streak .stat small { font-size: 9px; }
  .setting { margin-bottom: 0; }
  .setting__head { margin-bottom: 3px; }
  .cal__head { margin-bottom: 4px; }
}
