/* =========================================================================
   The map: a card per chapter, strung along a sea route.

   There used to be a drawn island on every card — nine shared shells with a
   landmark each, keyed by module slug. It went when chapters became something
   coaches create and delete from the staff panel: a new chapter had no entry
   in that table, so it either borrowed another island's landmark or arrived
   blank, and neither is a map. What is left says the same three things in a
   card half the height: which chapter, how big, how far in you are.
   ========================================================================= */

/* =========================================================================
   The island tile on the map
   ========================================================================= */
.isle-card {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 0.3rem;
  padding: 0.7rem 0.85rem 0.85rem;
  background: var(--paper);
  border: 2px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: 0 var(--lift) 0 var(--line);
  text-decoration: none;
  color: var(--ink);
  transition: transform var(--t-fast) var(--ease-out),
              box-shadow var(--t-fast) var(--ease-out),
              border-color var(--t-fast);
}
.isle-card:hover { color: var(--ink); transform: translateY(-3px); box-shadow: 0 7px 0 var(--line); }
.isle-card:active { transform: translateY(var(--lift)); box-shadow: 0 0 0 var(--line); }

.isle-card.is-current {
  border-color: var(--brand);
  box-shadow: 0 var(--lift) 0 var(--brand-deep);
}
.isle-card.is-complete { border-color: var(--grass); box-shadow: 0 var(--lift) 0 var(--grass-deep); }
.isle-card.is-locked {
  filter: grayscale(0.85);
  opacity: 0.6;
  cursor: not-allowed;
  pointer-events: none;
}

/* Not yet released, as opposed to not yet reached. Lighter than the locked
   card because this one is a promise rather than a refusal — the island is
   still meant to look inviting. Deliberately keeps pointer-events, so the
   "opens on release day" tooltip can actually be read; the locked card's
   tooltip has never been reachable for exactly that reason. */
.isle-card.is-soon {
  filter: grayscale(0.5);
  opacity: 0.72;
  cursor: default;
}

/* The number sits IN the card now rather than pinned to its corner: with no
   artwork there is no picture for it to overlap, and a corner badge on a card
   this short crowds the title it is supposed to introduce. */
.isle-top {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  inline-size: 100%;
}
.isle-top .isle-meta { margin-inline-end: auto; }

.isle-num {
  min-inline-size: 26px;
  padding: 0.1rem 0.4rem;
  border-radius: var(--r-pill);
  background: var(--canvas-deep);
  color: var(--ink-soft);
  font-weight: 900;
  font-size: 0.8rem;
  text-align: center;
}
/* White on --brand/--grass (#58cc02) measures 2.09:1 — a number nobody with
   ordinary eyesight reads comfortably. --ink-grass on the soft tint is 6.51:1
   and the ring keeps "this is the live one" unmistakable. */
.isle-card.is-complete .isle-num { background: var(--brand-soft); color: var(--ink-grass); }
.isle-card.is-current .isle-num  { background: var(--brand-soft); color: var(--ink-grass);
                                   box-shadow: 0 0 0 2px var(--brand); }

.isle-title {
  font-size: 0.98rem;
  font-weight: 800;
  line-height: 1.35;
  text-align: center;
  /* Two lines of a Persian title, held whatever the name is, so a row of
     cards does not step up and down along the route. */
  min-block-size: 2.7em;
  display: flex;
  align-items: center;
}
.isle-meta { font-size: 0.78rem; color: var(--ink-soft); }
.isle-status { font-size: 0.82rem; font-weight: 800; color: var(--ink-soft); }

.isle-bar {
  inline-size: 100%;
  block-size: 11px;
  box-sizing: border-box;
  border-radius: var(--r-pill);
  /* A GREY track, not a green outline. Green is what "done" looks like
     everywhere else on this site, so outlining an empty bar in it made a
     chapter nobody had touched read as finished — a full green pill sitting
     directly above the word "Start". The track still has to be visible
     against the white card, which is what the outline was really for, so it
     is drawn in the same grey as every other border here and filled a shade
     darker than the card rather than the same white. */
  border: 1.5px solid var(--line);
  background: var(--canvas-deep);
  overflow: hidden;
}
.isle-bar i {
  display: block;
  block-size: 100%;
  border-radius: inherit;
  background: var(--grass);
  transition: inline-size var(--t-mid) var(--ease-out);
}

.isle-flag { font-size: 1.05rem; line-height: 1; }



/* =========================================================================
   The voyage — islands strung along a winding sea route.
   ========================================================================= */

/* =========================================================================
   The voyage -- the route IS the map.
   -------------------------------------------------------------------------
   Everything here is in real pixels: the route svg is sized to its own
   viewBox (720 x H), so 1 unit = 1 CSS px on BOTH axes and a stroke width is
   a stroke width. No preserveAspectRatio, no vector-effect, nothing stretched.
   ========================================================================= */
.voyage {
  --card-w: 216px;
  position: relative;
  block-size: var(--h);       /* no padding: VOYAGE_LEAD_IN/OUT do that job,
                                 and border-box padding would clip the svg */
  margin-block: 1.5rem 1.25rem;
  border-radius: var(--r-xl);
  overflow: hidden;
  /* Open water: bright where the light hits it at the top, deepening as it
     goes down. The crests live on the two layers below, which move. */
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--sea) 38%, #fff) 0%,
    color-mix(in srgb, var(--sea) 78%, #fff) 18%,
    var(--sea) 52%,
    color-mix(in srgb, var(--sea) 76%, var(--sea-deep)) 100%);
}

/* The two crest fields, drifting at different speeds and in opposite
   directions. The parallax between them is the whole trick — one moving layer
   reads as wallpaper sliding past, two read as water.
   
   They animate TRANSFORM and nothing else. The sea this replaces animated
   background-position across two full-height pseudos, which repaints the whole
   map every frame and is why it was taken out; a transform is handed straight
   to the compositor and costs the same moving as it does still. Each layer is
   one tile wider than the map and travels exactly one tile, so the loop has no
   seam. z-index 0 keeps them under the route (1) and the cards (2), and the
   site-wide prefers-reduced-motion rule stops them both dead. */
.voyage::before,
.voyage::after {
  content: "";
  position: absolute;
  inset-block: 0;
  z-index: 0;
  pointer-events: none;
  will-change: transform;
}
.voyage::before {
  inset-inline-start: 0;
  inline-size: calc(100% + 226px);
  background: radial-gradient(ellipse 58px 7px at 50% 50%,
    rgb(255 255 255 / .92) 0 42%, transparent 72%) 0 0 / 226px 154px;
  animation: sea-crests 21s linear infinite;
}
.voyage::after {
  inset-inline-start: -173px;
  inline-size: calc(100% + 173px);
  background: radial-gradient(ellipse 38px 5px at 50% 50%,
    rgb(255 255 255 / .42) 0 42%, transparent 72%) 0 0 / 173px 211px;
  animation: sea-ripples 34s linear infinite;
}

@keyframes sea-crests  { to { transform: translate3d(-226px, 0, 0); } }
@keyframes sea-ripples { to { transform: translate3d(173px, 0, 0); } }

.voyage-route {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 50%;
  inline-size: var(--lane);
  block-size: var(--h);
  translate: -50% 0;
  z-index: 1;
}
/* Cards mirror themselves (inset-inline-start); an svg path does not. Mirror
   the whole drawing so the route still meets the islands it threads. The
   element is centred on the meridian and transform-origin is its own centre,
   so flipping it about itself is the same as flipping it about the page. */
[dir="rtl"] .voyage-route { translate: 50% 0; transform: scaleX(-1); }

/* NB: do NOT write `.voyage-route path { fill: none }` -- (0,1,1) beats the
   (0,1,0) of .rt-buoy and the buoys would vanish. Per-class, deliberately. */
.rt-channel, .rt-ahead, .rt-wake, .rt-jetty, .rt-goal-x { fill: none; }

/* --- the water the route runs through ---------------------------------- */
.rt-channel {
  stroke: var(--sea-pale);
  stroke-width: 30;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* --- the route ahead ----------------------------------------------------
   Thin and dashed, but 3.48:1 against the water. "Faint" is done with weight
   and pattern, never by making the next island hard to find. */
.rt-ahead {
  stroke: var(--sea-line);
  stroke-width: 5;
  stroke-linecap: round;
  stroke-dasharray: 5 15;
  opacity: .55;
  animation: rt-drift 3.4s linear infinite;
}
@keyframes rt-drift { to { stroke-dashoffset: -20; } }   /* exactly one period */

/* --- the route already sailed -------------------------------------------
   Gold with a dark rim: gold on this water is 1.07:1, so the rim carries the
   contrast (5.38:1) and the gold carries the charm.
   The dash is `--sailed --len`: ONE dash exactly as long as the distance
   already sailed, then a gap longer than everything left. Offsetting by
   --sailed hides it, so animating the offset to 0 draws the voyage on load. */
.rt-wake {
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: var(--sailed) var(--len);
  stroke-dashoffset: var(--sailed);
  animation: rt-draw 1.2s var(--ease-out) .15s forwards;
}
@keyframes rt-draw { to { stroke-dashoffset: 0; } }
.rt-wake--edge { stroke: var(--sea-deep); stroke-width: 12; }
.rt-wake--body { stroke: var(--wake);     stroke-width: 7.5; }
.rt-wake--core { stroke: #fffbe8;         stroke-width: 2.2; opacity: .85; }

/* --- ports --------------------------------------------------------------- */
.rt-jetty { stroke: var(--sea-deep); stroke-width: 3; stroke-linecap: round;
            stroke-dasharray: 2 6; opacity: .42; }
.rt-buoy  { fill: #fff; stroke: var(--sea-deep); stroke-width: 3.4; }
.rt-pip   { fill: var(--sea-line); }
.rt-port--complete .rt-buoy { stroke: var(--ink-grass); }
.rt-port--complete .rt-pip  { fill: var(--grass); }
.rt-port--current  .rt-buoy { fill: var(--wake); stroke: var(--sea-deep); }
.rt-port--current  .rt-pip  { fill: var(--sea-deep); }
.rt-port--locked .rt-buoy, .rt-port--soon .rt-buoy { stroke: var(--ink-faint); }
.rt-port--locked .rt-pip,  .rt-port--soon .rt-pip  { fill: var(--ink-faint); }
.rt-port--locked .rt-jetty, .rt-port--soon .rt-jetty { opacity: .2; }

/* Exactly one ring, on exactly one island, on the whole page. */
.rt-ping {
  fill: none; stroke: var(--wake); stroke-width: 3;
  transform-box: fill-box; transform-origin: center;
  animation: rt-ping 2.6s var(--ease-out) infinite;
}
@keyframes rt-ping {
  from { transform: scale(.55); opacity: .95; }
  to   { transform: scale(2.6); opacity: 0; }
}

.rt-goal-ring { fill: none; stroke: var(--sea-deep); stroke-width: 3.4;
                stroke-dasharray: 6 6; opacity: .5; }
.rt-goal-x    { stroke: var(--sea-deep); stroke-width: 4; stroke-linecap: round; opacity: .5; }
.rt-goal.is-won .rt-goal-ring,
.rt-goal.is-won .rt-goal-x { stroke: var(--wake); opacity: 1; }


/* --- the stops, docked either side of the channel -------------------------
   The card's INNER edge is pinned 136px off the meridian, 32px of clear water
   outside the route's 104px crest. The route is therefore NEVER hidden behind
   a card -- the old one ran through the card centres, so ~210px of every 562px
   segment was occluded and the only visible part was the flat middle.

   Cards alternate sides, so adjacent cards cannot overlap horizontally at any
   height, and same-column cards are 340px apart -- against a card that is now
   about 120px tall. VOYAGE_STEP is a density knob, not a collision margin. */
.voyage-stop {
  --haze: 0;
  position: absolute;
  inset-block-start: var(--y);
  inline-size: var(--card-w);
  z-index: 2;
  animation: voyage-arrive .5s var(--ease-bounce) both var(--d, 0s);
}
.voyage-stop--start { inset-inline-start: calc(50% - var(--dock)); translate: -100% -50%; }
.voyage-stop--end   { inset-inline-start: calc(50% + var(--dock)); translate: 0 -50%; }
/* In RTL inset-inline-start resolves to `right`, which anchors the card's
   other edge, so the translate flips with it. Verified: --start spans
   [50%W-328, 50%W-112] in LTR and [50%W+112, 50%W+328] in RTL. Exact mirror,
   one stylesheet. Widest case: at the 760px collapse threshold the container
   is 728px and a --end card reaches container x=692. 36px of slack. */
[dir="rtl"] .voyage-stop--start { translate: 100% -50%; }
@media (min-width: 1120px) { .voyage { --card-w: 232px; } }
@keyframes voyage-arrive { from { opacity: 0; transform: scale(.84); } }

/* Solid, opaque fill (backdrop-filter is gone). */
.isle-card { background: var(--paper); }

/* --- islands ahead: distance, not damage ---------------------------------
   grayscale(.85) + opacity(.6) said "broken", and composited --ink-soft down
   to 2.25:1 on the title, a straight WCAG failure. Aerial perspective says
   "far" instead: lay the sea's own colour over the card in proportion to how
   many islands ahead it is. --haze runs 1-6, so the veil runs .13 -> .38.

   The veil used to sit on the artwork, which kept it clear of the words by
   accident. With the artwork gone it covers the whole card, so it is pinned
   BEHIND the text: the point was always that a far island recedes, never that
   its name gets harder to read. */
.isle-card.is-locked { cursor: default; pointer-events: none; }
.isle-card.is-soon   { cursor: default; }
.isle-card.is-locked { background: var(--canvas-deep); border-color: var(--line); }
.isle-card.is-locked > *,
.isle-card.is-soon   > * { position: relative; z-index: 1; }
.isle-card.is-locked::before,
.isle-card.is-soon::before {
  content: "";
  position: absolute; inset: 0;
  z-index: 0;
  border-radius: inherit;
  background: var(--sea);
  opacity: calc(.08 + var(--haze) * .05);
  pointer-events: none;
}
/* Coming-soon is a promise, not a refusal -- half the haze of a locked one. */
.isle-card.is-soon::before { opacity: calc(.05 + var(--haze) * .028); }

/* --- the boat, at the boundary -------------------------------------------
   Not moored to a card any more: it stands on the route at arc length
   --sailed, exactly where gold becomes dashes. As the current island fills,
   place_voyage shortens the lead from 88px to 44px, so the boat visibly
   closes on it. */
.voyage-boat {
  position: absolute;
  inset-block-start: var(--by);
  inset-inline-start: calc(50% + var(--bx));
  inline-size: 76px;
  z-index: 3;
  translate: -50% -56%;
  rotate: calc(var(--tilt, 0deg) * var(--flip, 1));
  animation: voyage-bob 3.6s ease-in-out infinite;
  pointer-events: none;
}
/* translate, rotate and scale compose in that fixed order before `transform`,
   so `scale: -1 1` alone would mirror the hull but leave the lean on the
   wrong side. --flip negates the tangent angle too: together, a true mirror. */
[dir="rtl"] .voyage-boat { translate: 50% -56%; scale: -1 1; --flip: -1; }
@keyframes voyage-bob {
  0%, 100% { transform: translateY(0) rotate(-2deg); }
  50%      { transform: translateY(-6px) rotate(2deg); }
}

/* --- the legend ---------------------------------------------------------- */
.voyage-key {
  display: flex; flex-wrap: wrap; gap: .4rem 1.3rem;
  justify-content: center;
  margin: 0 0 2.2rem; padding: 0; list-style: none;
  font-size: .82rem; color: var(--ink-soft);
}
.voyage-key li { display: flex; align-items: center; gap: .4rem; }
.voyage-key .key {
  inline-size: 14px; block-size: 14px; border-radius: 50%;
  border: 3px solid var(--sea-deep); background: #fff;
}
/* Scoped to .voyage-key on purpose. The base rule above is (0,2,0); a bare
   .key--done is (0,1,0) and loses to it, so every marker in the legend drew
   as the same blue ring and the key explained nothing. */
.voyage-key .key--done { border-color: var(--ink-grass); background: var(--grass); }
.voyage-key .key--here { border-color: var(--sea-deep); background: var(--wake); }
.voyage-key .key--lock { border-color: var(--ink-faint); background: var(--canvas-deep); }


@media (max-width: 760px) {
  /* Phones get the plain list, deliberately.
     The desktop map earns its route: cards alternate either side of a channel
     and the wake shows how far you have sailed. None of that survives one
     column — the rail it collapsed to was a decoration running down the edge
     of a stack of cards, taking a lane of width away from the titles on the
     narrowest screens for no information the cards do not already carry.
     Each card states its own progress in words, a bar and a colour, so on a
     phone that IS the map. */
  .voyage {
    block-size: auto;
    display: grid;
    grid-template-columns: 1fr;
    padding: 1.1rem .8rem 1.4rem;
    border-radius: var(--r-lg);
    overflow: visible;
  }
  .voyage-route,
  .voyage-boat { display: none; }

  .voyage-stop, [dir="rtl"] .voyage-stop {
    position: relative; inset: auto; translate: none;
    inline-size: auto;
    grid-column: 1;
    grid-row: auto;
    padding-block: .4rem;
  }

  /* Full width, so the title reads as a line rather than a column. Nothing
     to lay the card out around any more, so it stays one stack. */
  .isle-card {
    justify-items: stretch;
    gap: .3rem;
    padding: .7rem .85rem;
  }
  .isle-title { text-align: start; min-block-size: 0; }
  .isle-status { text-align: start; }
  /* The key describes marks that only the desktop route draws. */
  .voyage-key { display: none; }
}

/* 320px: 288 page - 50 gutter - 13 padding leaves 225 of card, and with no
   art column all of it minus 26 of padding goes to the title. */
@media (max-width: 380px) {
  .isle-title { font-size: .9rem; }
}


@media (prefers-reduced-motion: reduce) {
  .isle-card *,
  .rt-ahead, .rt-ping, .voyage-stop, .voyage-boat,
  .pboat-rig, .pboat-water path, .pboat-pupil { animation: none !important; }

  /* MUST pin the offset too. .rt-wake is HIDDEN at stroke-dashoffset:
     var(--sailed) and revealed by the animation; stopping the animation
     without this erases the entire progress wake. */
  .rt-wake { animation: none !important; stroke-dashoffset: 0; }
}

/* Frozen, the map still has: the water band and its crest field, the pale
   channel, the gold wake stopping under Pity, the dashed route ahead, a buoy
   per island coloured by state, jetties, the goal marker, the horizon haze on
   everything you have not reached, and the legend. Nothing here depends on
   motion to read as a map. */

