/* =========================================================================
   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: 9px;
  border-radius: var(--r-pill);
  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;
  background:
    /* a static crest field. Two repeating radial-gradients on the element's
       own background, painted once. The old sea animated background-position
       on two full-height pseudos; water does not have to move for the page to
       read as water, and the dashes and the boat are the only motion here
       that means anything. */
    radial-gradient(ellipse 58px 7px at 50% 50%, rgb(255 255 255 / .85) 0 42%, transparent 72%) 0 0 / 226px 154px,
    radial-gradient(ellipse 38px 5px at 50% 50%, rgb(31 123 168 / .20) 0 42%, transparent 72%) 74px 61px / 173px 211px,
    linear-gradient(180deg,
      color-mix(in srgb, var(--sea) 55%, #fff) 0%,
      var(--sea) 40%,
      color-mix(in srgb, var(--sea) 84%, var(--sea-line)) 100%);
}

.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 paper again -- backdrop-filter is gone, and the water underneath is
   opaque, so there is nothing left for it to do. */
.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. */
