/* FILE VERSION: v88 */
/* Claude edit: 2026-08-22 - split Hangman's 5 shared color vars into 30 isolated per-element vars, no consolidation */
/* Claude edit: 2026-08-13 23:30 UTC - camelCase rename pass (classes, custom properties, storage keys) */
/* Claude edit: 2026-08-13 23:45 UTC - renamed from app.css, no functional changes */
:root {
  --navy: #1F1147;
  /* Exact colors from the hangman hologram's Three.js materials, so
     the on-screen keyboard's correct/wrong key states match the 3D
     figure's own cyan/red instead of picking from the hub palette. */
  --hgBgKeyCorrect: #B1BBCC;
  --hgBorderKeyCorrect: #B1BBCC;
  --hgBgKeyWrong: #ef4444;
  --hgBorderKeyWrong: #ef4444;
  --cream: #FFF3DC;
  --gold: #FFB800;
  /* Shared by box1/box3 (width) and box2 (right-edge offset) in the
     landscape media query below, so both reference the same single
     number instead of two independent guesses that could drift out
     of sync. Widened a little (was 300/40vw/420) so the keyboard
     has more room in landscape - takes that same amount away from
     box2 (the puzzle grid), since they share this one variable. */
  --cwSideW: clamp(340px, 44vw, 460px);
  --teal: #00C2A8;
  --violet: #7C5CFF;
  --orange: #FF8A3D;
  --sky: #5CC2FF;
  --pink: #FF5C8A;
  /* Darker (~70%) version of each hub color, for the crossword grid
     box background - the board rotates through these on every new
     puzzle (see .cwBoard0 through .cwBoard5 below). Same 0.70
     per-channel darken ratio applied to all 6, so they read as one
     consistent family rather than 6 unrelated picks. Each game's
     main box will get its own set the same way, as that game gets
     its board restyled. */
  --pinkDark: #B34061;
  --goldDark: #B28100;
  --tealDark: #008876;
  --violetDark: #5740B3;
  --orangeDark: #B2612B;
  --skyDark: #4088B3;
  /* Fixed light tan for the crossword grid's outer border/bevel -
     does not rotate with the board color, per Teresa's direction
     that the bevel stays a constant tan regardless of board color. */
   /* Hangman color palette. Every element has its own isolated
     variable - no sharing across elements, per Teresa's rule. */
  --hgBgPanel: #F1D9B3;
  --hgBgBoardWrap: #0A2F3A;
  --hgBgClue: #5A4839;
  --hgBgBackBtn: #5A4839;
  --hgBgScore: #5A4839;
  --hgBgDiff: #5A4839;
  --hgBgKey: #5A4839;
  --hgBgWordWrap: #5A4839;
  --hgBgKeyActive: #5A4839; /* PLACEHOLDER, needs lighter shade */
  --hgBorderBoardWrap: #D8975A;
  --hgBorderBackBtn: #D8975A;
  --hgBorderScore: #D8975A;
  --hgBorderDiff: #D8975A;
  --hgBorderDiffHard: #FAB800;
  --hgBorderClue: #D8975A;
  --hgBorderLetterBox: #D8975A;
  --hgBorderKey: #D8975A;
  --hgBorderWordWrap: #D8975A;
  --hgGlowBackBtn: #D8975A; /* PLACEHOLDER, not yet discussed */
  --hgGlowBoardWrap: #D8975A; /* PLACEHOLDER, not yet discussed */
  --hgGlowScore: #D8975A; /* PLACEHOLDER, not yet discussed */
  --hgGlowClue: #D8975A; /* PLACEHOLDER, not yet discussed */
  --hgGlowKey: #D8975A; /* PLACEHOLDER, not yet discussed */
  --hgGlowWordWrap: #D8975A; /* PLACEHOLDER, not yet discussed */
  --hgTextBackBtn: #1D6B78;
  --hgTextScore: #1D6B78;
  --hgTextDiff: #1D6B78;
  --hgTextClueLabel: #1D6B78;
  --hgTextClueText: #1D6B78;
  --hgTextKey: #1D6B78;
  --hgTextLetterBox: #0A2F3A; /* PLACEHOLDER, try Deep Current */
  --hgTextLetterStatic: #0A2F3A; /* PLACEHOLDER, try Deep Current */
  --hgTextMistakes: #0A2F3A; /* PLACEHOLDER, try Deep Current */
  --hgTextMistakesCompact: #0A2F3A; /* PLACEHOLDER, try Deep Current */
  --titleRed: #E8283F;
  --titleRedDeep: #B8283A;
  --tickerBlue: #1C4FD1;
  /* Crossword board: a success green (none of the 6 hub colors reads
     as true green - teal skews cyan), plus 6 tint versions of the
     hub colors (each hub color flattened ~22% over white) used for
     per-word color coding on the active word - see .cwWordColor*
     below. Silver bevel frame/lightened blocked cells from the last
     pass didn't read well on-device and have been dropped in favor
     of this per-word approach instead. */
  --success: #34C759;
  --tintPink: #FFDBE5;
  --tintGold: #FFEFC7;
  --tintTeal: #C7F2EC;
  --tintViolet: #E2DBFF;
  --tintOrange: #FFE5D4;
  --tintSky: #DBF2FF;
  /* Neutral shadow for anything sitting on the new navy game
     background - a navy drop-shadow would vanish against a navy
     backdrop, so keys/buttons/the grid box use this instead. Their
     border/glow colors (hub palette) are untouched - only the
     drop-shadow/ambient-glow layer switches to grey. */
  --shadowGrey: #B7BCC4;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html, body {
  height: 100%;
  overscroll-behavior: none;
}

body {
  background: var(--cream);
  font-family: 'Nunito', -apple-system, BlinkMacSystemFont, sans-serif;
  color: var(--navy);
  overflow: hidden;
}

/* Same fix as .gamePanel below: without this, #hub's own
   display:flex beats the browser's [hidden] behavior and the hub
   never actually disappears when a game panel opens. */
#hub[hidden] {
  display: none;
}

#hub {
  height: 100dvh;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: env(safe-area-inset-top) env(safe-area-inset-right)
           env(safe-area-inset-bottom) env(safe-area-inset-left);
  padding-top: clamp(10px, 2.4vh, 22px);
  padding-bottom: clamp(8px, 2vh, 18px);
  padding-left: max(12px, env(safe-area-inset-left));
  padding-right: max(12px, env(safe-area-inset-right));
  overflow: hidden;
}

.profileRow {
  width: 100%;
  max-width: min(1200px, 94vw);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px 12px;
  flex: none;
  margin-bottom: clamp(8px, 1.8vh, 18px);
}

.profileLeft {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
  order: 1;
}

.profileRight {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: none;
  order: 2;
}

.avatar {
  width: 52px;
  height: 52px;
  border-radius: 18px;
  background: var(--violet);
  border: 3px solid var(--navy);
  box-shadow: 3px 3px 0 var(--navy);
  display: grid;
  place-items: center;
  font: 26px/1 'Lilita One', cursive;
  color: #fff;
  flex: none;
}

.profileInfo {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 0;
}

.greeting {
  font-weight: 800;
  font-size: 13px;
  color: #7a6a4f;
}

.xpRow {
  display: flex;
  align-items: center;
  gap: 6px;
}

.xpBar {
  height: 10px;
  flex: 1;
  max-width: 120px;
  background: #EAD9B8;
  border-radius: 99px;
  overflow: hidden;
}

.xpFill {
  height: 100%;
  background: linear-gradient(90deg, var(--gold), var(--pink));
  border-radius: 99px;
}

.xpLevel {
  font-weight: 900;
  font-size: 12px;
  color: var(--navy);
  white-space: nowrap;
}

.gearBtn {
  width: 40px;
  height: 40px;
  border-radius: 99px;
  background: #fff;
  border: 3px solid var(--navy);
  box-shadow: 2px 2px 0 var(--navy);
  font-size: 18px;
  display: grid;
  place-items: center;
  cursor: pointer;
  flex: none;
  -webkit-tap-highlight-color: transparent;
}

/* Full width by default, so it wraps onto its own line below the
   avatar/gear row. The min-width:700px rule below overrides this
   to merge it inline instead. */
.brand {
  width: 100%;
  max-width: min(1200px, 94vw);
  text-align: center;
  flex: 1 1 100%;
  order: 3;
  margin-bottom: clamp(6px, 1.4vh, 14px);
}

.brand h1 {
  font-family: 'Lilita One', cursive;
  font-size: clamp(24px, 6.5vw, 44px);
  letter-spacing: 1px;
  color: var(--titleRed);
  text-shadow: 4px 4px 0 #C9C9C9;
  text-transform: uppercase;
  line-height: 1.05;
}

.brand p {
  font-weight: 800;
  font-size: clamp(10px, 2.4vw, 13px);
  color: var(--titleRedDeep);
  letter-spacing: 3px;
  text-transform: uppercase;
  margin-top: 3px;
}

.marqueeBanner {
  width: 100%;
  max-width: min(1200px, 94vw);
  background: var(--tickerBlue);
  border-radius: 16px;
  overflow: hidden;
  flex: none;
  margin-bottom: clamp(8px, 1.8vh, 20px);
}

.marqueeTrack {
  display: flex;
  white-space: nowrap;
  animation: marquee 9s linear infinite;
  padding: clamp(6px, 1.2vh, 9px) 0;
}

.marqueeTrack span {
  font-weight: 900;
  font-size: clamp(11px, 2.6vw, 13px);
  color: #d4d8dc;
  letter-spacing: 1px;
  padding-right: 12px;
}

/* Metallic silver look: gradient clipped to the text shape.
   Falls back to the flat silver color set above on browsers
   without background-clip:text support. */
@supports (background-clip: text) or (-webkit-background-clip: text) {
  .marqueeTrack span {
    background: linear-gradient(180deg, #f6f7f8 0%, #cdd2d6 45%, #eef1f2 58%, #9aa1a7 100%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
  }
}

@keyframes marquee {
  0%   { transform: translateX(0); }
  100% { transform: translateX(-50%); }
}

main {
  width: 100%;
  max-width: min(1200px, 94vw);
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-template-rows: repeat(3, minmax(0, 1fr));
  gap: clamp(8px, 1.6vh, 14px);
}

/* Tablet/desktop gets a third column - phones stay locked at 2 */
@media (min-width: 700px) {
  main {
    grid-template-columns: repeat(3, 1fr);
    grid-template-rows: repeat(2, minmax(0, 1fr));
    gap: 20px;
  }

  /* Covers anything 700px+ wide: tablets, desktop, and phones in
     landscape. Title merges into the top row since there's room. */
  .brand {
    width: auto;
    flex: 0 1 auto;
    order: 2;
    margin-bottom: 0;
    text-align: center;
  }
  .profileRight { order: 3; }

  .brand h1 {
    font-size: clamp(22px, 3.2vw, 36px);
  }
  .brand p {
    font-size: clamp(9px, 1.1vw, 12px);
    margin-top: 2px;
  }
}

.tile {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: clamp(4px, 1vh, 10px);
  text-decoration: none;
  color: #fff;
  border: 3px solid var(--navy);
  border-radius: 20px;
  box-shadow: 4px 5px 0 var(--navy);
  padding: clamp(6px, 1.4vh, 14px) 10px;
  min-height: 0;
  overflow: hidden;
  transform: rotate(var(--tilt, 0deg));
  transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.15s ease;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.tile:hover, .tile:focus-visible {
  transform: rotate(0deg) scale(1.04);
  box-shadow: 5px 6px 0 var(--navy);
}

.tile:active {
  transform: rotate(0deg) translateY(0);
  box-shadow: 1px 2px 0 var(--navy);
}

.icon {
  display: grid;
  gap: 3px;
}

.icon2x2 { grid-template-columns: repeat(2, 22px); grid-auto-rows: 22px; }
.iconRow { display: flex; gap: 3px; }

.letter {
  background: #fff;
  color: var(--navy);
  border-radius: 5px;
  display: grid;
  place-items: center;
  font-weight: 900;
  font-size: 12px;
  width: 22px;
  height: 22px;
}

.letterFill  { background: var(--navy); }
.letterLight { color: #b9a7ff; }

.emojiIcon { font: 34px/1 sans-serif; }

.hangmanIcon { width: 68px; height: 68px; }

.wsIcon {
  position: relative;
  width: 48px;
  height: 48px;
  border-radius: 11px;
  background: #fff;
  border: 3px solid var(--navy);
  display: grid;
  place-items: center;
}

.wsIcon span {
  font: 900 9.5px/1.4 'Nunito', sans-serif;
  color: var(--navy);
  letter-spacing: 1.5px;
}

.wsHighlight {
  position: absolute;
  top: 4px;
  left: 4px;
  right: 4px;
  height: 12px;
  border: 2px solid var(--pink);
  border-radius: 99px;
}

.tileTitle {
  font-family: 'Lilita One', cursive;
  font-size: clamp(15px, 3.4vh, 21px);
  color: #fff;
  text-shadow: 2px 2px 0 var(--navy);
  white-space: nowrap;
}

.tileSub {
  font-weight: 800;
  font-size: clamp(9px, 1.6vh, 10.5px);
}

.cardCrossword   { background: var(--pink);   --tilt: -2.4deg; }
.cardCrossword .tileSub   { color: #FFE0EB; }
.cardHangman     { background: var(--gold);   --tilt: 2.1deg; }
.cardHangman .tileSub     { color: #6b4d00; }
.cardWordsearch  { background: var(--teal);   --tilt: -1.8deg; }
.cardWordsearch .tileSub  { color: #00544a; }
.cardSudoku      { background: var(--violet); --tilt: 2.6deg; }
.cardSudoku .tileSub      { color: #DCD2FF; }
.cardBoggle      { background: var(--sky);    --tilt: -2.2deg; }
.cardBoggle .tileSub      { color: #dff3ff; }
.cardScramble    { background: var(--orange); --tilt: 1.9deg; }
.cardScramble .tileSub    { color: #fff0e0; }
.cardScramble .tileTitle  { font-size: 19px; }

/* Short-height viewports (phone/tablet landscape, browser chrome
   eating vertical space): header, ticker and tiles all shrink so
   six tiles plus the ticker fit with nothing cut off or hidden. */
@media (max-height: 560px) {
  #hub {
    padding-top: 4px;
    padding-bottom: 4px;
  }

  .profileRow { margin-bottom: 6px; }

  .avatar {
    width: 38px;
    height: 38px;
    border-radius: 12px;
    font-size: 18px;
    border-width: 2px;
    box-shadow: 2px 2px 0 var(--navy);
  }

  .greeting { font-size: 10.5px; }
  .xpBar { height: 7px; max-width: 90px; }
  .xpLevel { font-size: 10.5px; }

  .brand h1 { font-size: clamp(16px, 3.4vh, 24px); }
  .brand p { font-size: 8.5px; margin-top: 1px; }

  .gearBtn, .iconBtn {
    width: 32px;
    height: 32px;
    border-width: 2px;
  }

  .marqueeBanner {
    margin-bottom: 6px;
    border-radius: 10px;
  }

  .marqueeTrack { padding: 3px 0; }
  .marqueeTrack span { font-size: 9.5px; }

  main { gap: 6px; }

  .tile {
    padding: 3px 6px;
    gap: 1px;
    border-radius: 12px;
    border-width: 2px;
    box-shadow: 2px 3px 0 var(--navy);
  }

  .tileTitle { font-size: clamp(11px, 3vh, 15px); }
  .tileSub { font-size: 8px; }

  .icon2x2 { grid-template-columns: repeat(2, 15px); grid-auto-rows: 15px; }
  .letter { width: 15px; height: 15px; font-size: 8px; border-radius: 3px; }
  .emojiIcon { font-size: 22px; }
  .hangmanIcon { width: 44px; height: 44px; }
  .wsIcon { width: 30px; height: 30px; border-radius: 7px; }
  .wsIcon span { font-size: 6.5px; }
  .cardScramble .tileTitle { font-size: 13px; }
}

/* Game panels: each one is a full-screen view swapped in over the hub.
   The [hidden] override matters - without it, display:flex below beats
   the browser's built-in hidden behavior and every panel stacks in
   the page instead of actually disappearing. */
.gamePanel[hidden] {
  display: none;
}

.gamePanel {
  height: 100dvh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}

/* Box1, global for every game. Fixed height, never shrinks, always
   pins to the viewport top with a 15px gap - the same 15px used by
   box2/box3's gap and side padding below, so all three boxes share
   one spacing standard. Games may add elements inside box1 but
   should not remove the back button or title - box1's own size is
   meant to stay predictable across games. */
.gameTopbar {
  display: flex;
  align-items: center;
  gap: 12px;
  flex: none;
  padding: 15px 16px 14px;
}

.iconBtn {
  width: 40px;
  height: 40px;
  border-radius: 12px;
  border: 2px solid var(--navy);
  background: #fff;
  font-size: 18px;
  font-weight: 900;
  color: var(--navy);
  display: grid;
  place-items: center;
  cursor: pointer;
  flex: none;
  -webkit-tap-highlight-color: transparent;
}
.musicBtn {
  font-size: 26px;
}
.musicBtn[aria-pressed="false"] {
  opacity: 0.4;
}

/* One-time dismissible nudge for iPhone, where the Fullscreen API
   doesn't exist at all - points at Add to Home Screen instead,
   which is our actual fullscreen path on iOS. */
.iosTipBanner {
  position: fixed;
  bottom: max(14px, env(safe-area-inset-bottom));
  left: 50%;
  transform: translateX(-50%);
  max-width: 92vw;
  display: none;
  align-items: center;
  gap: 10px;
  padding: 8px 8px 8px 16px;
  background: var(--navy);
  border-radius: 999px;
  color: #fff;
  font-size: clamp(10px, 2.6vw, 12px);
  font-weight: 800;
  z-index: 200;
  box-shadow: 0 6px 20px rgba(31, 17, 71, 0.35);
}

.iosTipBanner.visible { display: flex; }

.iosTipClose {
  width: 22px;
  height: 22px;
  border-radius: 99px;
  border: none;
  background: rgba(255, 255, 255, 0.15);
  color: #fff;
  font-size: 14px;
  line-height: 1;
  display: grid;
  place-items: center;
  cursor: pointer;
  flex: none;
}

.modalClose {
  border-radius: 99px;
  font-size: 22px;
  line-height: 1;
}

.gameTopbar h2 {
  font-family: 'Lilita One', cursive;
  font-size: 22px;
  color: var(--navy);
}

/* Box2/box3 global template. Every game's box2 and box3 are direct
   children of .gameBody tagged data-box="2" / data-box="3" - that
   attribute, not a game ID, is the only hook this rule needs. Box3
   is flex:none - full natural height, whatever it contains, never
   compressed. Box2 is flex:1 with min-height/width:0, which is what
   actually lets it shrink all the way to zero - without that
   override a flex item's default min-height:auto would keep forcing
   the whole column taller than the viewport instead of letting box2
   give up its own space. This is the fix for the Hangman overflow
   bug, and it now applies to every game automatically, on any
   viewport, with no per-game measuring ever needed again. */
.gameBody {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 15px;
  padding: 0 15px 15px;
}

.gameBody [data-box="2"] {
  flex: 1;
  min-height: 0;
  min-width: 0;
}

.gameBody [data-box="3"] {
  flex: none;
}

.stubMessage {
  font-weight: 800;
  color: #000000;
  text-align: center;
}

/* Crossword */

/* Whole game screen goes navy - the same color that used to sit on
   just the blocked/unused tiles. Everything else in this section
   (keys, buttons, the clue box, the grid frame) stays cream so it
   reads as a raised card floating on this navy backdrop. */
#game-Crossword.gamePanel {
  background: var(--navy);
}

/* Shared .gameTopbar h2 rule is navy text, invisible on the new
   navy panel background - crossword needs its own light color here. */
#game-Crossword .gameTopbar h2 {
  color: var(--cream);
}

/* Scoped to #game-crossword so the hub and other games' back/icon
   buttons (shared .iconBtn class) are untouched. */
#game-Crossword .iconBtn {
  background: var(--cream);
  border: 3px solid var(--teal);
  color: #000;
  box-shadow: 0 0 8px 1px var(--shadowGrey);
}

.cwTimer {
  flex: none;
  white-space: nowrap;
  background: var(--cream);
  border: 3px solid var(--orange);
  border-radius: 99px;
  padding: 6px 12px;
  font: 900 13px 'Nunito', sans-serif;
  color: #8a8f98;
  box-shadow: 0 0 8px 1px var(--shadowGrey);
}

.cwDiff {
  flex: none;
  white-space: nowrap;
  background: var(--cream);
  border: 3px solid var(--violet);
  border-radius: 99px;
  padding: 6px 12px;
  font: 900 13px 'Nunito', sans-serif;
  color: #000;
  box-shadow: 0 0 8px 1px var(--shadowGrey);
  cursor: pointer;
}

.cwDiff.cwDiffHard {
  border-color: var(--sky);
  box-shadow: 0 0 8px 1px var(--shadowGrey);
}

.cwScore {
  flex: none;
  white-space: nowrap;
  background: var(--cream);
  border: 3px solid var(--gold);
  border-radius: 99px;
  padding: 6px 12px;
  font: 900 13px 'Nunito', sans-serif;
  color: #8a8f98;
  box-shadow: 0 0 8px 1px var(--shadowGrey);
}

.cwPause {
  flex: none;
  width: 34px;
  height: 34px;
  border-radius: 99px;
  background: var(--cream);
  border: 3px solid var(--teal);
  color: #000;
  font-size: 15px;
  display: grid;
  place-items: center;
  cursor: pointer;
  box-shadow: 0 0 8px 1px var(--shadowGrey);
  -webkit-tap-highlight-color: transparent;
}

/* Topbar layout: a left cluster (back button + pause + timer) and
   a right cluster (score + difficulty), pushed to opposite ends -
   shared by both the portrait full topbar and the landscape compact
   bar, so the two stay visually consistent. Plain flex + space-
   between (not a 3-column grid) so the right cluster isn't forced
   into an equal-width column with the left - it just gets whatever
   space is left over, room to grow as more pills join it later.
   Only .gameTopbar gets `display: flex` set here - its portrait/
   landscape show/hide toggle happens entirely through `display`
   overrides at equal (ID+class) specificity, so source order
   decides safely either way (both sides already agree on flex, so
   there's no conflict to worry about there). .cwTopbarCompact is
   NOT given `display` here on purpose: its portrait-hidden state
   comes from the older unscoped `.cwTopbarCompact { display:
   none; }` rule below, at lower specificity - if this rule also set
   `display` on it, that lower-specificity `none` could never win,
   and the compact bar would show in portrait too. Its own `display:
   flex` is set only inside the landscape media query. */
#game-Crossword .gameTopbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

#game-Crossword .cwTopbarCompact {
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.cwTopbarLeft {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
}

.cwTopbarRight {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  min-width: 0;
}

/* Game title removed from the topbar per Teresa's direction - the
   back button, timer, pause, score and difficulty pill are the only
   things shown there now. */
#game-Crossword .gameTopbar h2 {
  display: none;
}

/* Manual pause - dims and disables everything except the pause
   button itself (and the back button, so a paused game can still be
   exited). Belt-and-suspenders with the state.manualPaused guards
   in crossword.js's input handlers. */
#game-Crossword.cwPaused .cwGrid,
#game-Crossword.cwPaused .cwKeyboard,
#game-Crossword.cwPaused .cwDiff,
#game-Crossword.cwPaused .cwClueMore {
  pointer-events: none;
  opacity: 0.5;
}

/* Box2/box3's actual anchor mechanism. This is CSS Grid, not
   flexbox, and that's deliberate: the old approach nested box2 and
   box3 two levels deep inside a flex chain (gameBody > cwWrap >
   [box2, cwSide > box3]) that depended on height:100% resolving
   correctly through a flex-grown ancestor - a documented
   cross-engine inconsistency that kept causing box3 to get squeezed
   or box2 to overshoot depending on the browser. Grid's 1fr/auto
   row (portrait) or column (landscape) sizing doesn't have that
   percentage-through-flex-ancestor problem, and box2/box3 are now
   direct grid children with nothing wrapping them - no cwWrap
   div exists anymore. Scoped to crossword only - doesn't touch the
   shared .gameBody rule other games rely on. Portrait no longer
   needs a crossword-specific override here - the global box1/box2/
   box3 template in the shared .gameBody rule covers it. */
.cwSide {
  display: flex;
  flex-direction: column;
  gap: 14px;
  width: 100%;
  min-width: 0;
}

.cwBox3 {
  display: flex;
  flex-direction: column;
  gap: 14px;
  width: 100%;
  min-width: 0;
}

/* Landscape (fullscreen or not): the compact bar below carries the
   back button, timer and difficulty badge instead. Both bars are
   permanent DOM elements - only a plain orientation media query
   decides which one shows, so there's no JS timing to get wrong.
   See the landscape media query further down for the toggle. */
.cwTopbarCompact {
  display: none;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  min-width: 0;
  flex: none;
}

.cwTopbarCompact .backBtn {
  width: 34px;
  height: 34px;
  font-size: 18px;
}

/* Box2's flexible middle container. flex:1/min-height:0/min-width:0
   now come from the shared .gameBody [data-box="2"] rule - this
   selector only adds crossword's own visual centering plus its own
   local min-height/width:0 (harmless duplicate, kept so this rule
   still works standalone if the shared rule ever changes). It does
   not know or care about the puzzle at all. It's measured exactly
   once per puzzle (see measureGridSize() in crossword.js) - not
   continuously - and that measurement is what word selection fits
   itself to. The box itself never resizes for puzzle content; only
   an actual orientation flip changes its shape (via the grid-
   template swap further down), a pure data transpose on the JS
   side, not a re-measurement. */
.cwGridWrap {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
  min-width: 0;
  width: 100%;
  height: 100%;
}

/* width/height:100% - a plain rectangle, full stop, ALWAYS - this
   box's own outer shape never changes, on rotation or difficulty
   switch or anything else. Two earlier approaches - a JS pixel
   measurement and a pure aspect-ratio - both tried to make this
   BOX's own shape depend on the puzzle's cols/rows, which is
   backwards: the box draws first and the puzzle is chosen to fit
   it, not the other way around. That principle is unchanged here.
   What's different from the original 1fr version: grid-template-
   columns/rows are now a fixed pixel size (set by
   applyCellSizing() in crossword.js, re-measured on every render)
   instead of repeat(N, 1fr). 1fr guaranteed the tracks always fill
   this box exactly, but only produces square cells when this box's
   live aspect ratio happens to match rows:cols - which a rotation's
   transpose or a restored difficulty-tier snapshot can't always
   guarantee. Fixed-pixel tracks guarantee square cells unconditionally;
   justify-content/align-content: center below absorb whatever small
   leftover space results on one axis, which just reads as more of
   this box's own background - the same color a blocked/unused tile
   already uses, so it's never visible as a gap. */
.cwGrid {
  display: grid;
  width: 100%;
  height: 100%;
  justify-content: center;
  align-content: center;
  gap: 5px;
  /* Needed for .cwCellFlipIn's rotateX to read as a 3D tile flip
     instead of a flat squash. */
  perspective: 800px;
  /* Board color rotates every new puzzle - one of .cwBoard0
     through .cwBoard5 (added by crossword.js in startNewPuzzle())
     sets --boardDark to that game's chosen darker hub color for
     this round. --pinkDark fallback covers the first paint, before
     JS has added a board-color class. Border is a fixed light tan,
     independent of the rotating board color - see --tanBevel. */
  background: var(--boardDark, var(--pinkDark));
  border: 2px solid var(--tanBevel);
  border-radius: 16px;
  padding: 5px;
  /* Sunken bevel: dark shadow hugging the top/left inner edge, a
     faint highlight on the bottom/right - the recessed-panel version
     of a bevel, opposite of the old raised/protruding pipe-rim look.
     Uniform on all 4 sides (no offset drop-shadow this time, which
     was what made the left/bottom edge read as offset before) - just
     these 2 inset rings plus the plain border above. */
  box-shadow:
    inset 2px 2px 5px rgba(0, 0, 0, 0.4),
    inset -1px -1px 2px rgba(255, 255, 255, 0.25);
}

/* Board color rotation - crossword.js adds exactly one of these to
   .cwGrid in startNewPuzzle(), cycling 0-5 each new puzzle. Each
   just sets the --boardDark custom property; .cwGrid's own
   background and .cwCell.blocked both read from it, so board +
   blocked cells always match. */
.cwGrid.cwBoard0 { --boardDark: var(--pinkDark); }
.cwGrid.cwBoard1 { --boardDark: var(--goldDark); }
.cwGrid.cwBoard2 { --boardDark: var(--tealDark); }
.cwGrid.cwBoard3 { --boardDark: var(--violetDark); }
.cwGrid.cwBoard4 { --boardDark: var(--orangeDark); }
.cwGrid.cwBoard5 { --boardDark: var(--skyDark); }

.cwCell {
  min-width: 0;
  background: var(--cream);
  border-radius: 5px;
  display: grid;
  place-items: center;
  position: relative;
  font: 900 clamp(13px, 3.2vw, 19px) 'Nunito', sans-serif;
  color: var(--navy);
  cursor: pointer;
  transition: transform 0.18s ease-in;
}

.cwCell.blocked {
  /* Matches the grid's own rotating background (--boardDark)
     instead of the cell cream - blocked squares should read as "no
     tile here", same as the classic black-square convention, not as
     a playable tile in a different color. --boardDark is set on
     the ancestor .cwGrid and inherits down to these cells, so both
     always match without extra JS work per cell. */
  background: var(--boardDark, var(--pinkDark));
  cursor: default;
}

/* Per-word color coding (Option C): each word cycles through the 6
   hub colors by its index (state.activeWordIdx % 6, set in
   crossword.js), flattened to a light tint so navy letters stay
   readable. Replaces the old flat single-color activeWord
   highlight - the point is that switching between two different
   active words now visibly changes color, not just which cells
   are lit up. */
.cwCell.activeWord.cwWordColor0 { background: var(--tintPink); }
.cwCell.activeWord.cwWordColor1 { background: var(--tintGold); }
.cwCell.activeWord.cwWordColor2 { background: var(--tintTeal); }
.cwCell.activeWord.cwWordColor3 { background: var(--tintViolet); }
.cwCell.activeWord.cwWordColor4 { background: var(--tintOrange); }
.cwCell.activeWord.cwWordColor5 { background: var(--tintSky); }

.cwCell.active {
  box-shadow: 0 0 0 3px var(--pink) inset;
}

.cwCell.cwCorrect {
  box-shadow: 0 0 0 3px var(--success) inset;
}

.cwCell.cwWrong {
  box-shadow: 0 0 0 3px var(--titleRed) inset;
}

/* Plays once when a word is completed correctly - see
   checkWordCompletionsForCell() in crossword.js. --stagger is set
   per-cell (JS) to that cell's position in the word, so the glow
   visibly runs letter by letter rather than flashing all at once. */
.cwCellCelebrate {
  animation: cwCelebrate 1.1s ease-out;
  animation-delay: var(--stagger, 0ms);
}

@keyframes cwCelebrate {
  0% {
    transform: scale(1);
    box-shadow: 0 0 0 3px var(--success) inset;
  }
  40% {
    transform: scale(1.15);
    box-shadow: 0 0 14px 4px var(--success), 0 0 0 3px var(--success) inset;
  }
  100% {
    transform: scale(1);
    box-shadow: 0 0 0 3px var(--success) inset;
  }
}

/* Plays once when a word is completed but wrong - a longer shake
   that decays out, no stagger (the whole word wobbles together,
   reading as one rejected unit rather than a per-letter effect). */
.cwCellShake {
  animation: cwShake 0.8s ease-in-out;
}

/* Difficulty-switch redraw - see redrawForDifficulty() and
   renderGrid()'s flipReveal param in crossword.js. --stagger is
   row-based (bottom row first), same mechanism as .cwCellCelebrate
   above. animation-fill-mode: backwards holds each cell at the 0%
   state during its delay, so nothing pops in early. */
.cwCellFlipIn {
  animation: cwFlipIn 0.4s ease-out;
  animation-delay: var(--stagger, 0ms);
  animation-fill-mode: backwards;
}

@keyframes cwFlipIn {
  0% { transform: rotateX(90deg); opacity: 0; }
  60% { transform: rotateX(-8deg); opacity: 1; }
  100% { transform: rotateX(0deg); }
}

@keyframes cwShake {
  0%, 100% { transform: translateX(0); }
  8% { transform: translateX(-6px); }
  16% { transform: translateX(6px); }
  24% { transform: translateX(-5px); }
  32% { transform: translateX(5px); }
  42% { transform: translateX(-4px); }
  52% { transform: translateX(4px); }
  62% { transform: translateX(-3px); }
  72% { transform: translateX(3px); }
  82% { transform: translateX(-1px); }
  90% { transform: translateX(1px); }
}

.cwNum {
  position: absolute;
  top: 2px;
  left: 4px;
  font: 800 8px 'Nunito', sans-serif;
  color: var(--violet);
}

.cwClue {
  position: relative;
  background: var(--cream);
  border: 3px solid var(--sky);
  border-radius: 16px;
  padding: 12px 16px;
  box-shadow: 0 0 10px 1px var(--sky);
}

.cwClueLabel {
  font: 900 11px 'Nunito', sans-serif;
  color: #8a8f98;
  letter-spacing: 2px;
  min-height: 1.3em;
}

/* Fixed to exactly one line on purpose - box3's height must never
   depend on how long a clue's text happens to be, or box2 (which
   fills whatever's left above box3) has no stable target. Long
   clues truncate with an ellipsis; renderClue() in crossword.js
   shows the "more" button only when that actually happens. */
.cwClueText {
  font: 800 15px 'Nunito', sans-serif;
  color: var(--navy);
  margin-top: 2px;
  padding-right: 28px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Small toggle, only shown when the clue is actually truncated.
   Tapping it expands the full clue as a popover (position:absolute)
   layered over the board below - it never changes box3's own
   layout height, which is the whole point. */
.cwClueMore {
  position: absolute;
  top: 10px;
  right: 12px;
  width: 22px;
  height: 22px;
  border: 3px solid var(--sky);
  border-radius: 99px;
  background: var(--cream);
  color: #000;
  font-size: 10px;
  line-height: 1;
  display: grid;
  place-items: center;
  cursor: pointer;
  box-shadow: 0 0 6px 1px var(--sky);
}

.cwClueText.cwClueExpanded {
  position: absolute;
  top: 100%;
  left: 16px;
  right: 16px;
  margin-top: 6px;
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  background: var(--cream);
  border: 3px solid var(--sky);
  border-radius: 12px;
  padding: 8px 12px;
  box-shadow: 0 0 10px 1px var(--sky);
  z-index: 20;
}

.cwKeyboard {
  display: flex;
  flex-direction: column;
  gap: 11px;
  min-height: 0;
  min-width: 0;
}

.cwKeyRow {
  display: flex;
  gap: 6px;
  justify-content: center;
  min-height: 0;
}

.cwKey {
  flex: 1;
  max-width: 34px;
  /* Flat px, not vh-relative. 5.5vh gave a SMALLER key in landscape
     than portrait on the same phone, because vh is the viewport's
     short side in landscape - the height was silently orientation-
     dependent even though nothing here should be. A fixed px value
     is the only unit that's genuinely identical in both. */
  height: 38px;
  /* Cream reads as a clear raised button against the navy game
     background. --cwKeyAccent is set per-key below, cycling
     through the hub's 6 game-card colors, for the border only - the
     shadow/glow is a separate, fixed grey (see box-shadow below).
     Border 3px - was 6px, Teresa flagged that as too thick. */
  background: var(--cream);
  border: 3px solid var(--cwKeyAccent, #d8d2c4);
  border-radius: 16px;
  display: grid;
  place-items: center;
  font: 900 15px 'Nunito', sans-serif;
  color: #000;
  /* Soft grey glow instead of hard extrusion - this is CSS, not a
     real bloom pass, so it's a blurred grey shadow, not brightness-
     based. Inset highlight keeps a puffy, domed look on the face. */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.6),
    0 0 10px 1px var(--shadowGrey);
  cursor: pointer;
  /* Removes the default mobile browser tap-highlight flash (shows as
     a teal/blue rectangle on tap) - same convention already used on
     .tile and other buttons elsewhere in this file. */
  -webkit-tap-highlight-color: transparent;
}

/* 6 vibrant tones cycled across each row, matching the hub's six
   game-card colors - nth-child resets per row, so this repeats
   every 6 keys within a row, not across the whole keyboard. */
.cwKey:nth-child(6n+1) { --cwKeyAccent: var(--violet); }
.cwKey:nth-child(6n+2) { --cwKeyAccent: var(--gold); }
.cwKey:nth-child(6n+3) { --cwKeyAccent: var(--orange); }
.cwKey:nth-child(6n+4) { --cwKeyAccent: var(--sky); }
.cwKey:nth-child(6n+5) { --cwKeyAccent: var(--teal); }
.cwKey:nth-child(6n) { --cwKeyAccent: var(--pink); }

.cwKey:active {
  /* Press fills the key with its own accent color instead of the
     old gold-outline extrusion collapse. */
  background: var(--cwKeyAccent, #d8d2c4);
  transform: translateY(4px);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.15);
}

.cwActionRow {
  margin-top: 2px;
}

.cwKeyAction {
  max-width: none;
  height: clamp(32px, 6vh, 48px);
  font-size: 13px;
  letter-spacing: 0.3px;
  color: #000;
  background: var(--cream);
}

.cwKeyHint {
  --cwKeyAccent: var(--teal);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.6),
    0 0 10px 1px var(--shadowGrey);
}

/* Grey out + disable once hint uses run out this puzzle - resets
   on the next puzzle. Button text also swaps to "USED" in JS
   (updateHintDisplay() in crossword.js) so the disabled state reads
   clearly, not just as a faded button. */
.cwKeyHint.cwHintDisabled {
  opacity: 0.4;
  pointer-events: none;
  cursor: default;
}

.cwKeyBack {
  --cwKeyAccent: var(--pink);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.6),
    0 0 10px 1px var(--shadowGrey);
}

.cwKeyEnter {
  --cwKeyAccent: var(--violet);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.6),
    0 0 10px 1px var(--shadowGrey);
}

.cwKeyAction:active {
  transform: translateY(4px);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.15);
}

.cwKeyToggle {
  max-width: 46px;
  font-size: 10px;
  letter-spacing: -0.2px;
}

.cwWinTime {
  font: 800 15px 'Nunito', sans-serif;
  color: var(--navy);
  margin-bottom: 14px;
}

.cwBtn {
  width: 100%;
  background: var(--cream);
  color: #000;
  border: 3px solid var(--pink);
  border-radius: 14px;
  padding: 14px;
  font: 900 15px 'Nunito', sans-serif;
  box-shadow: 0 0 10px 1px var(--shadowGrey);
  cursor: pointer;
}

.cwBtn:active {
  transform: translateY(3px);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.15);
}

@media (max-width: 480px) {
  .cwKey {
    font-size: 13px;
  }

  .cwKeyAction {
    font-size: 11px;
  }
}

/* Short viewports: small phones, or any phone without fullscreen
   (browser chrome eats vertical space). Shrinks paddings/gaps/keys
   so the grid, clue and keyboard fit without scrolling. Scoped to
   #game-crossword only - other game panels are untouched. */
@media (max-height: 760px) {
  #game-Crossword .gameTopbar {
    padding: 8px 12px;
  }

  #game-Crossword .gameTopbar h2 {
    font-size: 18px;
  }

  #game-Crossword .cwTimer,
  #game-Crossword .cwDiff {
    padding: 4px 9px;
    font-size: 11px;
  }

  #game-Crossword .gameBody {
    padding: 0 10px 15px;
    gap: 8px;
  }

  #game-Crossword .cwGrid {
    padding: 6px;
    gap: 3px;
    border-radius: 12px;
  }

  #game-Crossword .cwNum {
    font-size: 7px;
  }

  #game-Crossword .cwClue {
    padding: 8px 12px;
    border-radius: 12px;
  }

  #game-Crossword .cwClueLabel {
    font-size: 9px;
  }

  #game-Crossword .cwClueText {
    font-size: 12px;
    margin-top: 1px;
  }

  #game-Crossword .cwKeyboard,
  #game-Crossword .cwKeyRow {
    gap: 8px;
  }

  #game-Crossword .cwKey {
    font-size: 12px;
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.5),
      0 2px 0 rgba(0, 0, 0, 0.25),
      0 4px 0 rgba(0, 0, 0, 0.15),
      0 5px 4px rgba(0, 0, 0, 0.25);
  }

  #game-Crossword .cwKey:active,
  #game-Crossword .cwKeyAction:active {
    transform: translateY(4px);
    box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.4);
  }

  #game-Crossword .cwKeyAction {
    font-size: 11px;
  }
}

/* Landscape anchor model: box1, box2, and box3 are genuinely
   independent here - each is position:fixed against the viewport
   itself, not a flex/grid participant depending on what its
   siblings do. That's the whole fix: every previous landscape bug
   came from box2 and box3 being sized relative to EACH OTHER
   (flex-grow splitting leftover space, or a Grid track measuring
   one against the other) - however carefully that relationship was
   tuned, a new edge case kept breaking it. Position:fixed removes
   the relationship entirely. Box1 pins to the top-right, box3 pins
   to the bottom-right, both sized by their own content only (no
   stretching, no growing) and sharing --cwSideW so they line up
   as a column. Box2 pins to all of top/left/bottom and stops short
   of the right edge by exactly --cwSideW plus gaps - a fixed
   calc(), not a measurement. #game-crossword .cwSide becomes
   `display:contents` - its children are the ones actually
   positioned, cwSide itself no longer needs to be a layout box at
   all. The min-width:680px clause below matches the same compound
   query crossword.js uses to decide when to transpose - they must
   stay in sync or CSS and JS could disagree about which orientation
   is active. */
@media (orientation: landscape) and (min-width: 680px) {
  /* The only place portrait/landscape topbar choice is made - a
     plain media query, not a JS class, so there's nothing for a
     script to race against. */
  #game-Crossword .gameTopbar {
    display: none;
  }

  #game-Crossword .cwTopbarCompact {
    display: flex;
    position: fixed;
    top: 15px;
    right: 15px;
    width: var(--cwSideW);
    z-index: 5;
  }

  /* cwSide no longer participates in gameBody's grid at all - its
     children (box1-compact, box3) are independently fixed below, so
     cwSide itself doesn't need to be a box in the layout. */
  #game-Crossword .cwSide {
    display: contents;
  }

  #game-Crossword .gameBody {
    display: block;
    position: relative;
    padding: 0;
  }

  #game-Crossword .cwGridWrap {
    position: fixed;
    /* Shrunk from 15px on the three free edges (top/left/bottom) -
       box2 has no sibling on those sides in landscape, so this was
       pure unused margin. Reclaiming it gives longer words (e.g. a
       10-letter word) more room to fit without shrinking tiles. The
       right edge still needs the gap to box3/the keyboard column. */
    top: 4px;
    left: 4px;
    bottom: 4px;
    right: calc(var(--cwSideW) + 20px);
    /* The base rule sets width/height:100% for portrait's grid
       track. CSS gives explicit width priority over left+right
       offsets when both are present, so without this override box2
       ignored the boundary above and rendered at the full viewport
       width regardless, with box1/box3 simply floating on top of
       it. auto here means "let top/left/right/bottom decide the
       size" instead. */
    width: auto;
    height: auto;
  }

  /* Box3: pinned to the bottom-right by itself, sized purely by its
     own content (clue + keyboard, both now fixed px, no vh) - not
     stretched or measured against box1 or box2. This is also why
     it renders at the same height as portrait: same DOM, same CSS,
     no viewport-relative units left in the chain to make it differ. */
  #game-Crossword .cwBox3 {
    position: fixed;
    bottom: 15px;
    right: 15px;
    width: var(--cwSideW);
    z-index: 5;
  }
}

.settingsBody {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 4px 0 4px;
}

.modalBackdrop {
  position: fixed;
  inset: 0;
  z-index: 150;
  background: rgba(31, 17, 71, 0.55);
  backdrop-filter: blur(3px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
}

.modalBackdrop.open {
  opacity: 1;
  pointer-events: all;
}

.modalCard {
  position: relative;
  width: clamp(280px, 86vw, 380px);
  max-height: 80vh;
  overflow-y: auto;
  background: var(--cream);
  border: 3px solid var(--navy);
  border-radius: 22px;
  box-shadow: 6px 8px 0 var(--navy);
  padding: 18px 18px 22px;
  transform: scale(0.92);
  transition: transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);
}

.modalBackdrop.open .modalCard {
  transform: scale(1);
}

.modalTopbar {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
}

.modalTopbar h2 {
  flex: 1;
  font-family: 'Lilita One', cursive;
  font-size: 22px;
  color: var(--navy);
}

.settingRow {
  background: #fff;
  border: 3px solid var(--navy);
  border-radius: 18px;
  padding: 16px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  box-shadow: 3px 4px 0 var(--navy);
}

.settingTitle {
  font-weight: 900;
  font-size: 15px;
  color: var(--navy);
}

.settingSub {
  font-weight: 700;
  font-size: 11px;
  color: #9a8a6a;
  margin-top: 2px;
}

/* Landscape only: settings pills sit side by side instead of
   stacked. The modal widens a bit so three pills have room. */
@media (orientation: landscape) {
  .modalCard {
    width: clamp(280px, 86vw, 640px);
  }

  .settingsBody {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 10px;
  }

  .settingRow { padding: 12px; }
  .settingTitle { font-size: 13px; }
  .settingSub { font-size: 9.5px; }
}

.toggle {
  width: 50px;
  height: 28px;
  border-radius: 99px;
  border: 3px solid var(--navy);
  background: #EAD9B8;
  position: relative;
  cursor: pointer;
  flex: none;
  -webkit-tap-highlight-color: transparent;
}

.toggle[aria-pressed="true"] { background: var(--teal); }

.toggleKnob {
  position: absolute;
  top: 1px;
  left: 1px;
  width: 20px;
  height: 20px;
  border-radius: 99px;
  background: #fff;
  border: 2px solid var(--navy);
  transition: left 0.15s ease;
}

.toggle[aria-pressed="true"] .toggleKnob { left: 25px; }

.settingSelect {
  font-family: 'Nunito', sans-serif;
  font-weight: 800;
  font-size: 13px;
  color: var(--navy);
  background: var(--cream);
  border: 3px solid var(--navy);
  border-radius: 12px;
  padding: 6px 10px;
  flex: none;
  max-width: 140px;
}
