/* =========================================================================
   WISE FISHERMAN - logged-in app. Shares the lookbook tokens so login,
   dashboard, settings, billing and history feel like one brand.
   Light & airy / authoritative. Mobile-first.

   Contrast note: the brand fills (--fair, --sunrise, --sea-bright) are tuned
   for AREAS, not small text. Every token has an "-ink" partner that clears
   WCAG AA (>=4.5:1) on paper/cloud; text uses the ink variant, fills use the
   brand variant. Never set small type in a raw fill color.
   ========================================================================= */
:root{
  --ink:#0C2A39; --sea:#15536E; --sea-bright:#2C7E9B;
  --seaglass:#A9CFCF; --seaglass-soft:#DCEAE9;
  --paper:#FAFAF7; --cloud:#FFFFFF; --mist:#EDF2F2; --line:#D9E2E2;
  --slate:#2F3D44; --slate-soft:#5E6E75;
  --sunrise:#E89A45; --sunrise-deep:#C97E2E; --sunrise-soft:#FBEAD6;
  --prime:#2F8F76; --fair:#DE9F39; --rough:#C2543E;

  /* AA-safe text partners (checked against --paper #FAFAF7) */
  --link-ink:#15536E;      /* 8.1:1  - body links (was --sea-bright, 4.47:1) */
  --prime-ink:#1F6E58;     /* 4.9:1  - go / favorable */
  --fair-ink:#8A5B14;      /* 5.7:1  - marginal (was --fair 2.3:1) */
  --rough-ink:#9E3B28;     /* 5.9:1  - rough / caution */

  --font-display:'Fraunces',Georgia,serif;
  --font-body:'Hanken Grotesk',system-ui,-apple-system,sans-serif;
  --font-mono:'IBM Plex Mono',ui-monospace,monospace;
  --radius:18px; --radius-sm:10px; --radius-pill:999px;
  --shadow-card:0 1px 0 rgba(12,42,57,.03), 0 26px 48px -34px rgba(12,42,57,.30);
  --tap:44px;              /* minimum interactive target */

  /* Fluid shell. There is deliberately NO pixel cap on .wrap: the map, the
     tables, the charts and the outcomes console are data surfaces and a
     2560px display should not render a 980px app. What stays measured is
     PROSE, and it is measured in CHARACTERS - a ch measure tracks the
     reader's font size, a px one silently gets narrower as type grows. */
  --gutter:clamp(16px,3vw,44px);
  --measure:72ch;          /* body prose */
  --measure-form:78ch;     /* label grids - four fields still land readable */
  --measure-narrow:46ch;   /* single-column forms: login, billing, delete */
  --scroll-cover:var(--paper);  /* pan-shadow cover; see .table-scroll */
}
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
/* THE SHELL IS A COLUMN, so the footer is a floor and not a divider. It
   lives in normal flow after <main>, so a page with little content - the
   dashboard while its forecast is still building, an empty history - left
   it stranded a third of the way down with the rest of the window blank
   under it. Worse with the working screen: that is viewport-centred and
   fixed, so it landed BELOW the footer and the page read nav / gap /
   footer / logo, in that order.
   main takes the slack, so the footer sits at the bottom of the viewport on
   a short page and directly under the content on a long one. */
body{margin:0;background:var(--paper);color:var(--slate);
  font-family:var(--font-body);font-size:16px;line-height:1.6;
  -webkit-font-smoothing:antialiased;position:relative;overflow-x:hidden;
  display:flex;flex-direction:column;
  min-height:100vh;min-height:100svh}
/* The CONTENT wrap only - the header has its own .wrap inside <header>, and
   body > is what tells them apart without inventing a second class. */
body>.wrap{flex:1 0 auto;display:flex;flex-direction:column}
body>.wrap>main{flex:1 0 auto}
body::before{content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;
  background:
    radial-gradient(900px 520px at 85% -10%, rgba(232,154,69,.12), transparent 60%),
    radial-gradient(1000px 600px at 0% 0%, rgba(169,207,207,.22), transparent 58%),
    linear-gradient(180deg,#FBFBF8, var(--paper) 40%);}
/* Full width, gutters that grow with the window. The @supports block adds the
   notch/home-indicator insets on top - in landscape on a notched phone the
   left inset is the difference between the first character being readable and
   being under the camera cutout. Split out because a browser with no env()
   would throw away the whole declaration and leave the page edge-to-edge. */
.wrap{width:100%;margin:0 auto;padding:0 var(--gutter)}
@supports(padding:max(0px)){
  .wrap{padding-left:max(var(--gutter),env(safe-area-inset-left,0px));
        padding-right:max(var(--gutter),env(safe-area-inset-right,0px))}
}
a{color:var(--link-ink);text-decoration:none}
a:hover{text-decoration:underline}

/* The working screen ---------------------------------------------------- */
/* Shown by loading.js on the page being LEFT while a slow server-rendered
   destination cooks. Brand paper, the mark, a ring, one rotating line -
   the wait reads as work instead of a hang. Never rendered without JS. */
/* z-index 25: UNDER the app header (30) and the footer (26), over the
   content. The chrome stays visible and clickable while the build cooks -
   a reader can leave for Settings mid-wait - and only the stale content is
   veiled. Anything on a page that must not poke through the veil needs its
   own stacking context (see .map-box). */
.working{position:fixed;inset:0;z-index:25;background:var(--paper);
  display:grid;place-items:center;
  /* Top pad clears the sticky header; the larger bottom pad lifts the
     stack clear of the footer, which floats ABOVE the veil (z 26) and on a
     short page sits high enough to collide with a centred line. */
  padding:64px 24px 160px;
  opacity:0;transition:opacity .25s ease}
/* THE LINE THAT MAKES THE PAGE CLICKABLE. An author display beats the
   browser's own [hidden]{display:none}, so without this the overlay is an
   invisible full-screen layer over EVERY page, eating every real mouse
   click while programmatic .click() sails through - which is exactly how a
   probe can pass while the product is unusable. hidden must always win. */
.working[hidden]{display:none}
/* Belt to that braces: any state that is not fully shown lets clicks
   through, so a half-armed overlay can never trap the page. */
.working:not(.is-on){pointer-events:none}
.working.is-on{opacity:1}
.working-inner{display:grid;place-items:center;gap:26px;text-align:center;
  padding:24px}
.working-inner svg{display:block}
.working-ring{width:64px;height:64px;border-radius:50%;
  border:4px solid var(--seaglass-soft);border-top-color:var(--sea);
  animation:working-spin 1s linear infinite}
@keyframes working-spin{to{transform:rotate(360deg)}}
.working-line{margin:0;font-family:var(--font-display);
  font-size:clamp(1.35rem,2.4vw,1.8rem);
  color:var(--ink);opacity:1;transition:opacity .24s ease;min-height:1.6em}
.working-line.is-fading{opacity:0}
@media(prefers-reduced-motion:reduce){
  .working{transition:none}
  .working-ring{animation:none;border-top-color:var(--seaglass-soft)}
  .working-line{transition:none}
}

/* Accessibility -------------------------------------------------------- */
:focus-visible{outline:2px solid var(--sea);outline-offset:2px;border-radius:4px}
.skip{position:absolute;left:-9999px;top:env(safe-area-inset-top,0px);
  z-index:100;background:var(--ink);
  color:var(--paper);padding:12px 18px;border-radius:0 0 var(--radius-sm) 0;
  font-weight:600}
.skip:focus{left:0;text-decoration:none}
.visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* App shell: header / brand / nav --------------------------------------- */
/* padding-top carries the status-bar inset so the sticky bar's own background
   fills the notch strip instead of the brand sliding up under the clock. */
.app-header{position:sticky;top:0;z-index:30;
  padding-top:env(safe-area-inset-top,0px);
  background:rgba(250,250,247,.88);backdrop-filter:blur(10px);
  border-bottom:1px solid var(--line)}
.app-header-inner{display:flex;align-items:center;justify-content:space-between;
  gap:12px;min-height:64px}
.brand{display:inline-flex;align-items:center;gap:10px;
  font-family:var(--font-display);font-size:1.25rem;color:var(--ink);
  text-decoration:none;padding:6px 0}
.brand:hover{text-decoration:none}
.brand svg{flex:0 0 auto}
.brand-word{white-space:nowrap}

.nav-toggle{display:inline-flex;align-items:center;justify-content:center;
  width:var(--tap);height:var(--tap);margin:0;padding:0;background:transparent;
  border:1px solid var(--line);border-radius:var(--radius-sm);
  color:var(--ink);cursor:pointer;box-shadow:none}
.nav-toggle:hover{background:var(--mist);transform:none}
.nav-toggle svg{width:22px;height:22px}
.nav-toggle .ico-close{display:none}
.nav-toggle[aria-expanded="true"] .ico-open{display:none}
.nav-toggle[aria-expanded="true"] .ico-close{display:block}

.app-nav{display:none}
.app-nav.open{display:block}
.app-nav{position:absolute;left:0;right:0;top:100%;background:var(--cloud);
  border-bottom:1px solid var(--line);box-shadow:var(--shadow-card);padding:8px 0}
.app-nav ul{list-style:none;margin:0;padding:0 var(--gutter)}
.app-nav li{border-top:1px solid var(--line)}
.app-nav li:first-child{border-top:none}
.app-nav a{display:flex;align-items:center;min-height:var(--tap);
  color:var(--slate);font-weight:600;font-size:1rem;text-decoration:none}
.app-nav a:hover{color:var(--ink);text-decoration:none}
.app-nav a[aria-current="page"]{color:var(--ink)}
.app-nav a[aria-current="page"]::before{content:"";width:4px;height:18px;
  border-radius:2px;background:var(--sunrise);margin-right:10px}

@media(min-width:768px){
  .nav-toggle{display:none}
  .app-nav,.app-nav.open{display:block;position:static;background:none;
    border:none;box-shadow:none;padding:0}
  .app-nav ul{display:flex;align-items:center;gap:22px;padding:0}
  .app-nav li{border:none}
  /* Still var(--tap), not auto: 768-1024px is a tablet, and a 30px-tall
     nav link is a finger target there, not a mouse one. It fits inside the
     64px header, so the bar does not get taller. */
  .app-nav a{min-height:var(--tap);font-size:.92rem;padding:8px 0}
  .app-nav a[aria-current="page"]{border-bottom:2px solid var(--sunrise)}
  .app-nav a[aria-current="page"]::before{display:none}
}

/* Main + headings ------------------------------------------------------ */
main{display:block;padding-bottom:8px}
.page-head{margin:26px 0 4px}
h1,h2,h3{font-family:var(--font-display);color:var(--ink);font-weight:400;
  line-height:1.15}
h1{font-size:clamp(1.7rem,5vw,2.3rem);margin:0 0 6px;letter-spacing:-.01em}
h2{font-size:clamp(1.35rem,4vw,1.6rem);margin:26px 0 10px}
h3{font-size:1.12rem;margin:22px 0 8px;border-bottom:1px solid var(--line);
  padding-bottom:6px}
.sub{color:var(--slate-soft);font-size:.88rem}
.dim{color:var(--slate-soft)}
.narrow{max-width:var(--measure-narrow)}
/* The reading measure. Now that the shell is fluid, prose is the only thing
   that must NOT take the width it is given - 200 characters a line is
   unreadable. A max-width wider than the element's own container is a no-op,
   so this is safe to state broadly: it bites on a full-width paragraph and
   does nothing inside a tile or a table cell. */
main p,main li,.msg,.sub,.field-hint,.blurb{max-width:var(--measure)}
h1,h2{max-width:26ch}
.eyebrow{font-family:var(--font-mono);font-size:11.5px;letter-spacing:.16em;
  text-transform:uppercase;color:var(--sea)}

/* Forms ---------------------------------------------------------------- */
label{display:block;margin:12px 0;font-size:.9rem;color:var(--slate)}
input,select,textarea{width:100%;margin-top:5px;padding:12px;
  background:var(--cloud);border:1px solid var(--line);
  border-radius:var(--radius-sm);color:var(--ink);
  font-size:16px;  /* 16px stops iOS Safari zooming on focus */
  font-family:var(--font-body);min-height:var(--tap)}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--sea);
  box-shadow:0 0 0 3px rgba(21,83,110,.15)}
input[type=checkbox]{width:auto;min-height:0;margin:0 10px 0 0}
.field-hint{display:block;color:var(--slate-soft);font-size:.8rem;margin-top:4px}
fieldset{border:none;margin:0;padding:0;min-width:0}

/* Buttons -------------------------------------------------------------- */
button,.btn{display:inline-flex;align-items:center;justify-content:center;
  gap:8px;margin-top:14px;padding:12px 22px;min-height:var(--tap);
  background:var(--sunrise);color:var(--ink);border:1px solid transparent;
  border-radius:var(--radius-pill);font-weight:600;font-size:.95rem;
  cursor:pointer;text-decoration:none;font-family:var(--font-body);
  box-shadow:0 12px 26px -16px rgba(232,154,69,.9);
  transition:transform .15s,background .2s}
button:hover,.btn:hover{background:var(--sunrise-deep);transform:translateY(-1px);
  text-decoration:none}
.btn-ghost,button.ghost{background:var(--mist);color:var(--sea);
  box-shadow:none;border-color:var(--line)}
.btn-ghost:hover,button.ghost:hover{background:var(--seaglass-soft)}
.btn-danger{background:var(--rough);color:#fff;box-shadow:none}
.btn-danger:hover{background:var(--rough-ink)}
.btn-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
/* A disclosure control is a button and gets a button's target. Height comes
   from padding rather than display:flex, because any display other than
   list-item deletes the browser's own disclosure marker. */
summary{cursor:pointer;min-height:var(--tap);padding:9px 0}
@media(prefers-reduced-motion:reduce){
  button,.btn{transition:none}
  button:hover,.btn:hover{transform:none}
}

/* Layout grids --------------------------------------------------------- */
/* Form grids get the form measure, not the window: a latitude field 900px
   wide on a desktop is harder to fill in than one 220px wide, and the four
   zone columns still land side by side inside it. */
.grid2{display:grid;grid-template-columns:1fr;gap:0 18px;
  max-width:var(--measure-form)}
.grid4{display:grid;grid-template-columns:1fr;gap:0 12px;
  max-width:var(--measure-form)}
@media(min-width:640px){
  .grid2{grid-template-columns:1fr 1fr}
  .grid4{grid-template-columns:2fr 1fr 1fr 1fr}
}

/* ---- the settings sheet ------------------------------------------------ */
/* One narrow stack of fields on a 2000px monitor left two thirds of the
   screen empty. Past ~1000px the settings form becomes a 12-column sheet:
   the map takes the right half beside the short home-port block, the boat
   fields sit under it, and species and delivery run the full width because
   chips and cards are what actually want the room.

   PLACEMENT IS EXPLICIT, and it follows DOM order exactly (port, map, boat,
   species, delivery -> left, right, left, full, full). Nothing is reordered:
   a form whose tab order disagrees with what the eye reads is a trap, and
   the fields themselves keep their own measures - a latitude box 900px wide
   is harder to fill in than one 220px wide. Below the breakpoint this rule
   does not apply and the page is exactly what it was. */
/* 1000px, not 1150: the map needs about 540px to be worth looking at and
   the field column about 400px, so a 1000px laptop - or a browser sitting at
   a zoom level that lands there - is already wide enough for the sheet, and
   a breakpoint set above it hands that window the empty half. */
@media(min-width:1000px){
  .set-grid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));
    column-gap:44px;align-items:start}
  .set-sec-port{grid-column:1/6;grid-row:1}
  /* The map spans the left-hand rows AND STICKS. Spanning is what removes
     the dead space on the left (the rows keep their own heights, so the
     boat block starts directly under the port block instead of after the
     map). Sticky is what removes it on the right: a map you refer to while
     you fill the rest of the form should still be on screen when you get to
     the fields that talk about it, rather than scrolling away and leaving a
     column of paper behind. align-self:start keeps the box its own height
     inside the taller area it spans. */
  /* The map's column is reserved for the WHOLE form, not just the first
     two rows: a sticky element slides within its containing block, and
     with the species and delivery sections spanning the full width the
     stuck map rode down the page and sat on top of the species picker -
     the second field report of the day. Every text section keeps to the
     left columns; the map may stick beside all of them because nothing
     else is ever in its lane. */
  .set-sec-map{grid-column:6/13;grid-row:1/span 4;
    position:sticky;top:14px;align-self:start}
  .set-sec-boat{grid-column:1/6;grid-row:2}
  .set-sec-species{grid-column:1/6;grid-row:3}
  .set-sec-delivery{grid-column:1/6;grid-row:4}
  /* The first heading in each column starts flush with its neighbour. */
  .set-sec>h3:first-child{margin-top:0}
  /* Two send-time cards, side by side once there is room. */
  .set-cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
    gap:0 18px;align-items:start;max-width:var(--measure-form)}
}

/* Chips (species select) ----------------------------------------------- */
.chips{display:flex;flex-wrap:wrap;gap:8px}
.chip{display:inline-flex;align-items:center;margin:0;background:var(--mist);
  border:1px solid var(--line);border-radius:var(--radius-pill);
  padding:10px 15px;min-height:var(--tap);font-size:.9rem;color:var(--slate);
  cursor:pointer}
.chip:hover{background:var(--seaglass-soft)}
.chip:has(input:checked){background:var(--sunrise-soft);
  border-color:var(--sunrise);color:var(--ink);font-weight:600}
.chip:focus-within{outline:2px solid var(--sea);outline-offset:2px}
.species-group{margin:0 0 14px}
.species-group-label{display:block;margin:0 0 7px;color:var(--sea)}

/* Flash messages ------------------------------------------------------- */
.flash{padding:12px 16px;border-radius:var(--radius-sm);margin:12px 0;
  font-size:.92rem;border:1px solid transparent}
.flash.ok{background:rgba(47,143,118,.10);color:var(--prime-ink);
  border-color:rgba(47,143,118,.25)}
.flash.error{background:rgba(194,84,62,.10);color:var(--rough-ink);
  border-color:rgba(194,84,62,.25)}
.flash.warn{background:var(--sunrise-soft);color:var(--fair-ink);
  border-color:rgba(232,154,69,.35)}

/* Cards / panels ------------------------------------------------------- */
.card{background:var(--cloud);border:1px solid var(--line);
  border-radius:var(--radius);box-shadow:var(--shadow-card);
  padding:16px 18px;margin:12px 0}
.card.notice{border-left:3px solid var(--sunrise)}
.msg{white-space:pre-wrap;overflow-wrap:anywhere;background:var(--cloud);
  border:1px solid var(--line);border-radius:var(--radius);padding:16px 18px;
  font-size:.95rem;box-shadow:var(--shadow-card)}
.cards{display:flex;flex-wrap:wrap;gap:10px}
.pick{border-left:3px solid var(--sunrise)}
.pick-date{font-family:var(--font-display);font-weight:500;color:var(--ink)}

/* Wide surfaces pan inside their own box - the PAGE never scrolls sideways.
   ------------------------------------------------------------------------
   One shared affordance for every wide thing on the site (tables, charts,
   the plan card). The four gradients are the classic pair of covers +
   shadows: background-attachment:local pins the covers to the CONTENT and
   scroll pins the shadows to the BOX, so a shadow is visible on exactly the
   edge that still has content behind it and vanishes when you reach the end.
   That makes it honest at every width - it appears because the surface
   really overflows, not because the viewport is under some breakpoint.
   Each container is also focusable in the markup, so the pan is reachable
   from a keyboard and not only from a finger; hence the focus ring. */
.table-scroll,.chart-scroll,.plan-scroll{
  overflow-x:auto;-webkit-overflow-scrolling:touch;
  background-image:
    linear-gradient(to right,var(--scroll-cover),rgba(255,255,255,0)),
    linear-gradient(to left,var(--scroll-cover),rgba(255,255,255,0)),
    linear-gradient(to right,rgba(12,42,57,.17),rgba(12,42,57,0)),
    linear-gradient(to left,rgba(12,42,57,.17),rgba(12,42,57,0));
  background-position:left center,right center,left center,right center;
  background-repeat:no-repeat;
  background-size:26px 100%,26px 100%,12px 100%,12px 100%;
  background-attachment:local,local,scroll,scroll}
.table-scroll:focus-visible,.chart-scroll:focus-visible,
.plan-scroll:focus-visible{outline:2px solid var(--sea);outline-offset:2px}

/* Tables: always scrollable rather than clipped on narrow screens ------- */
.table-scroll{margin-top:6px;border-radius:var(--radius-sm)}
.table-scroll table.grid{margin-top:0}
table.grid{width:100%;border-collapse:collapse;font-size:.82rem;margin-top:6px;
  min-width:520px}
table.grid th,table.grid td{border:1px solid var(--line);padding:8px;
  text-align:center;white-space:nowrap}
table.grid th{background:var(--mist);color:var(--ink);
  font-family:var(--font-mono);font-size:.72rem;letter-spacing:.08em;
  text-transform:uppercase;font-weight:500}
table.grid tr.today td{background:var(--sunrise-soft)}
table.grid td b{font-family:var(--font-mono);color:var(--ink)}
.scroll-hint{font-family:var(--font-mono);font-size:10.5px;
  color:var(--slate-soft);margin-top:4px}
@media(min-width:768px){.scroll-hint{display:none}}

/* Condition badges (color is always paired with a word) ----------------- */
.badge{display:inline-block;padding:2px 10px;border-radius:var(--radius-pill);
  font-family:var(--font-mono);font-size:.72rem;font-weight:500;
  letter-spacing:.04em}
.b-good{background:rgba(47,143,118,.14);color:var(--prime-ink)}
.b-great{background:var(--seaglass-soft);color:var(--sea)}
.b-unsafe{background:rgba(194,84,62,.14);color:var(--rough-ink)}
.b-marg{background:var(--sunrise-soft);color:var(--fair-ink)}
.b-unknown{background:var(--mist);color:var(--slate-soft)}

/* History -------------------------------------------------------------- */
.hist{background:var(--cloud);border:1px solid var(--line);
  border-radius:var(--radius);padding:4px 16px;margin-bottom:8px;
  box-shadow:var(--shadow-card)}
.hist summary{cursor:pointer;color:var(--ink);min-height:var(--tap);
  display:flex;align-items:center;gap:8px}
.hist summary::marker{color:var(--sea)}
.hist .msg{margin:0 0 14px;box-shadow:none}

/* Footer --------------------------------------------------------------- */
/* The bottom pad carries the home-indicator inset so the last link is not
   sitting under the swipe bar on a notched device. */
.app-footer{position:relative;z-index:26;
  margin:40px 0 0;border-top:1px solid var(--line);
  padding:20px 0 calc(34px + env(safe-area-inset-bottom,0px));
  color:var(--slate-soft);font-size:.82rem}
.app-footer a{color:var(--slate)}
.foot-legal{margin-top:10px}
.foot-links{display:flex;flex-wrap:wrap;gap:4px 16px;margin-top:10px}
.foot-links a{min-height:var(--tap);display:inline-flex;align-items:center}

/* The conditions map's symbol key, inside the legend card. */
.map-symbolkey{display:flex;flex-wrap:wrap;gap:4px 12px;margin-top:8px;
  padding-top:8px;border-top:1px solid var(--line);font-size:.78rem;
  color:var(--slate-soft)}
.map-symbolkey .lg{gap:5px}
.lg-buoy{width:10px;height:10px;border-radius:50%;background:#fff;
  border:2px solid var(--sea);box-sizing:border-box;flex:none}
.lg-zone{width:16px;height:16px;border-radius:50%;background:var(--sunrise);
  color:#fff;font-size:10px;font-weight:700;display:inline-flex;
  align-items:center;justify-content:center;flex:none}

/* Structure markers on the conditions map: squares for charted records,
   filled for reefs - dots already mean buoys there. */
.mk-struct{width:9px;height:9px;background:#fff;
  border:2px solid var(--sea);box-sizing:border-box;
  box-shadow:0 0 0 1px rgba(255,255,255,.7)}
.mk-struct-reef{background:var(--seaglass)}

/* ---- Lookbook forecast panel (dashboard) ------------------------------ */
.fc-panel{background:linear-gradient(180deg,#FFFFFF,#FCFDFC);
  border:1px solid var(--line);border-radius:calc(var(--radius) + 4px);
  box-shadow:var(--shadow-card);padding:18px;margin:8px 0 18px}
@media(min-width:768px){.fc-panel{padding:26px}}
.fc-top{display:flex;justify-content:space-between;align-items:flex-start;
  gap:16px;flex-wrap:wrap}
.fc-headline{font-family:var(--font-display);font-weight:400;color:var(--ink);
  font-size:clamp(22px,5vw,32px);line-height:1.12;margin:8px 0 0}
.fc-pills{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:14px}
.verdict{font-family:var(--font-mono);font-weight:600;letter-spacing:.06em;
  font-size:1.05rem;padding:5px 15px;border-radius:var(--radius-pill)}
.v-go{background:rgba(47,143,118,.14);color:var(--prime-ink)}
.v-fair{background:var(--sunrise-soft);color:var(--fair-ink)}
.v-rough{background:rgba(194,84,62,.16);color:var(--rough-ink)}
.v-unknown{background:var(--mist);color:var(--slate-soft)}
.pill{display:inline-flex;align-items:center;gap:7px;
  font-family:var(--font-mono);font-size:12px;font-weight:500;
  letter-spacing:.04em;padding:7px 13px;border-radius:var(--radius-pill)}
.pill .dot{width:7px;height:7px;border-radius:50%;flex:0 0 auto}
.pill-prime{background:rgba(47,143,118,.12);color:var(--prime-ink)}
.pill-prime .dot{background:var(--prime)}
.pill-warn{background:rgba(194,84,62,.14);color:var(--rough-ink)}
.pill-warn .dot{background:var(--rough)}
/* alert = caution you plan around (e.g. scattered storms), not a no-go */
.pill-alert{background:var(--sunrise-soft);color:var(--fair-ink)}
.pill-alert .dot{background:var(--sunrise)}

/* Safety band: hazards render ABOVE best-window language, never below. */
.fc-hazards{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px;
  padding:12px 14px;border-radius:var(--radius-sm);
  background:rgba(194,84,62,.07);border:1px solid rgba(194,84,62,.22)}
.fc-hazards.alert-only{background:var(--sunrise-soft);
  border-color:rgba(232,154,69,.35)}
.fc-hazards .hz-label{font-family:var(--font-mono);font-size:10.5px;
  letter-spacing:.14em;text-transform:uppercase;color:var(--rough-ink);
  width:100%;margin-bottom:2px}
.fc-hazards.alert-only .hz-label{color:var(--fair-ink)}
/* A hazard chip that opens onto the warning's own text. Closed it is the same
   chip; open it takes the full width of the band and drops the issuing
   office's wording underneath. Native <details>, so the chip is clickable and
   tab-reachable with no script - the +/- is the whole affordance, because a
   chip that gives no sign it opens is the defect this replaced. */
.hz-open{max-width:100%}
.hz-open[open]{width:100%}
.hz-open>summary{cursor:pointer;list-style:none}
.hz-open>summary::-webkit-details-marker{display:none}
.hz-open>summary::after{content:"+";font-weight:700;opacity:.7}
.hz-open[open]>summary::after{content:"-"}
.hz-text{margin:8px 2px 0;font-size:13px;line-height:1.55;color:var(--ink)}
.hz-src{display:inline-block;margin:4px 2px 0;font-family:var(--font-mono);
  font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--slate-soft)}

/* auto-fit rather than a hard 4: the dashboard has four tiles and gets four
   columns exactly as before, but the outcomes console has six and used to
   wrap 4 + 2 on a display with room for all six on one line. */
.fc-tiles{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:20px}
@media(min-width:700px){
  .fc-tiles{grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:12px}
}
.fc-tiles .tile{background:var(--cloud);border:1px solid var(--line);
  border-radius:var(--radius-sm);padding:14px 16px}
.fc-tiles .lbl{font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--slate-soft)}
.fc-tiles .val{font-family:var(--font-mono);font-size:26px;font-weight:500;
  color:var(--ink);margin-top:6px;line-height:1}
.fc-tiles .val small{font-size:13px;color:var(--slate-soft);font-weight:400}
.fc-tiles .sub{font-size:12.5px;color:var(--slate-soft);margin-top:6px}

/* ---- Per-spot row (multi-zone accounts) ------------------------------- */
.fc-spots{display:grid;grid-template-columns:1fr;gap:8px;margin-top:20px}
@media(min-width:700px){.fc-spots{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}}
.fc-spots .eyebrow{grid-column:1/-1;margin-bottom:2px}
.fc-spots .spot{background:var(--cloud);border:1px solid var(--line);
  border-radius:var(--radius-sm);padding:12px 14px}
.fc-spots .spot-best{border-color:var(--slate-soft)}
.fc-spots .spot-head{display:flex;justify-content:space-between;
  align-items:center;gap:8px}
.fc-spots .spot-head b{color:var(--ink);font-size:14.5px;min-width:0;
  overflow-wrap:anywhere}
.fc-spots .verdict{font-size:11px;padding:3px 8px;flex:none}
.fc-spots .sub{font-size:12.5px;color:var(--slate-soft);margin-top:6px}

/* ---- Unified day-timeline graph (tide + rain + storms + prime + seas) -- */
.fc-timeline{margin-top:22px}
.fc-tl-head{display:flex;justify-content:space-between;align-items:baseline;
  flex-wrap:wrap;gap:8px}
.fc-tl-legend{font-family:var(--font-mono);font-size:11.5px;
  color:var(--slate-soft);display:flex;flex-wrap:wrap;align-items:center;gap:10px}
.fc-tl-legend .lg{display:inline-block;width:14px;height:0;border-top-width:2px;
  border-top-style:solid;margin-right:3px;vertical-align:middle}
.fc-tl-legend .lg-tide{border-top-color:var(--sea)}
.fc-tl-legend .lg-rain{border-top-style:dashed;border-top-color:var(--sea-bright)}
.fc-tl-legend .lg-prime{height:9px;border:0;background:var(--sunrise);
  opacity:.5;border-radius:2px}
.fc-tl-legend .lg-storm{height:9px;border:0;background:var(--rough);
  opacity:.4;border-radius:2px}
.fc-tl-svg{width:100%;height:160px;display:block;margin-top:8px;
  background:linear-gradient(180deg,rgba(169,207,207,.10),transparent 70%);
  border-radius:var(--radius-sm)}
/* sea-state strip colors (paired with the text label below for a11y) */
.fc-tl-svg .sea-flat{fill:var(--prime);opacity:.55}
.fc-tl-svg .sea-light-chop{fill:var(--fair);opacity:.5}
.fc-tl-svg .sea-moderate{fill:var(--sunrise-deep);opacity:.55}
.fc-tl-svg .sea-rough{fill:var(--rough);opacity:.6}
.fc-tl-svg .sea-unknown{fill:var(--line);opacity:.5}
.fc-tl-axis{display:flex;justify-content:space-between;
  font-family:var(--font-mono);font-size:11px;color:var(--slate-soft);
  margin-top:2px}
.fc-tl-marks{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
.fc-tl-marks .tl-mark{font-family:var(--font-mono);font-size:11.5px;
  color:var(--slate-soft);background:var(--mist);border-radius:var(--radius-pill);
  padding:4px 11px}
.fc-tl-marks .tl-prime{background:var(--sunrise-soft);color:var(--fair-ink)}
.fc-tl-marks .tl-sea{background:rgba(47,143,118,.12);color:var(--prime-ink)}

/* ---- Three decision scores (safety / fishability / comfort) ------------ */
/* Business plan: three distinct scores, never blended. Color is paired with
   a text word (Favorable/Marginal/Unfavorable) - never color alone. */
.fc-scores{display:grid;grid-template-columns:1fr;gap:10px;margin-top:22px}
@media(min-width:640px){
  .fc-scores{grid-template-columns:repeat(3,1fr);gap:12px}
}
.score{border:1px solid var(--line);border-radius:var(--radius-sm);
  padding:14px 16px;border-left-width:3px;background:var(--cloud)}
.score .lbl{font-family:var(--font-mono);font-size:11px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--slate-soft)}
.score .val{font-family:var(--font-mono);font-size:30px;font-weight:500;
  color:var(--ink);margin-top:4px;line-height:1}
.score .word{font-family:var(--font-mono);font-size:12.5px;font-weight:600;
  letter-spacing:.04em;margin-top:6px}
.score .blurb{font-size:12.5px;color:var(--slate-soft);margin-top:8px;
  line-height:1.5}
.score.s-go{border-left-color:var(--prime)}
.score.s-go .word{color:var(--prime-ink)}
.score.s-fair{border-left-color:var(--fair)}
.score.s-fair .word{color:var(--fair-ink)}
.score.s-rough{border-left-color:var(--rough)}
.score.s-rough .word{color:var(--rough-ink)}
.score.s-unknown{border-left-color:var(--line)}
.score.s-unknown .word{color:var(--slate-soft)}
.fc-meta{font-family:var(--font-mono);font-size:11.5px;color:var(--slate-soft);
  margin-top:14px;line-height:1.6}

/* Locked capability teaser (e.g. raw-data viewer for free users) */
.card.locked{border-style:dashed;background:var(--mist);margin-top:18px}
.card.locked b{color:var(--ink)}

/* ---- The callout (webapp/templates/_callout.html) ----------------------
   The one component for every empty, locked or unknown state: icon, one
   bold line naming what is missing, one plain line with the REASON, an
   optional plain line with the UNLOCK, an optional action. Dashed like the
   old locked card - "state, not verdict" - with the kind carried by the
   icon and a quiet left accent, never colour alone (the words say which
   kind it is). */
.callout{display:flex;gap:14px;align-items:flex-start;
  border:1px dashed var(--line);border-left:3px solid var(--line);
  border-radius:var(--radius-sm);background:var(--mist);
  padding:16px 18px;margin-top:18px}
.callout .co-icon{flex:none;width:26px;height:26px;margin-top:1px;
  color:var(--slate-soft)}
.callout .co-icon svg{display:block;width:100%;height:100%}
.callout .co-text{min-width:0}
.callout .co-head{margin:0;font-weight:600;color:var(--ink);line-height:1.4}
.callout .co-reason{margin:6px 0 0;font-size:13.5px;color:var(--slate);
  line-height:1.55}
.callout .co-unlock{margin:6px 0 0;font-size:13.5px;color:var(--slate-soft);
  line-height:1.55}
.callout .co-act{margin:12px 0 0}
.co-locked{border-left-color:var(--slate-soft)}
.co-unknown{border-left-color:var(--fair)}
.co-wait{border-left-color:var(--sea)}
.co-empty{border-left-color:var(--sea)}

/* ---- "How this works" explainer (webapp/templates/_howitworks.html) ---- */
.howworks{margin-top:18px;border:1px solid var(--line);
  border-radius:var(--radius-sm);background:var(--cloud);padding:12px 18px}
.howworks summary{cursor:pointer;font-weight:600;color:var(--sea)}
.howworks .how-steps{margin:12px 0 0;padding-left:22px}
.howworks .how-steps li{margin-top:8px;font-size:13.5px;line-height:1.55}
.howworks .how-table-head{margin:16px 0 0;font-family:var(--font-mono);
  font-size:11px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--slate-soft)}
.how-table td:first-child{font-weight:600;color:var(--ink)}

/* ---- Forecast graphics (server-rendered SVG, webapp/charts.py) ---------- */
/* The chart module emits no colors of its own - every ch- mark resolves here
   against the brand tokens. The drawings have a fixed 720-unit viewBox, so
   scaling them scales the type with the picture rather than clipping it:
   - min-width:720px is the LEGIBILITY floor. Below it the 10.5px ch-sub
     labels would drop under the type floor, so the chart pans inside
     .chart-scroll instead of being squeezed.
   - width:100% lets it grow past 720 on a wide window, which is the whole
     point of the fluid shell: at 1440px the wind trace gets 1300px of
     detail instead of stopping at half the screen.
   - the 2x ceiling is about the DRAWING, not the layout. The viewBox scales
     uniformly, so 3.5x on a 2560px display would also make an 11px label
     38px tall and a 200-unit chart 700px deep. Twice native is more hours
     per inch; past that it is just a bigger picture of the same data. The
     container still spans the page. */
.chart-scroll{margin-top:10px;border-radius:var(--radius-sm)}
svg.ch{display:block;width:100%;min-width:720px;max-width:calc(720px * 2);
  height:auto;
  background:linear-gradient(180deg,rgba(169,207,207,.10),transparent 70%);
  border-radius:var(--radius-sm)}
.ch text{font-family:var(--font-mono);fill:var(--slate)}
.ch .ch-lbl{font-size:11px;fill:var(--slate)}
.ch .ch-sub{font-size:10.5px;fill:var(--slate-soft)}  /* the type floor */
.ch-grid{stroke:var(--line);stroke-width:1}
.ch-daybreak{stroke:var(--seaglass)}
.ch-axis{stroke:var(--slate-soft);stroke-width:1}
/* Wind trace = the body link ink; the gust band is the same ink at area
   strength, so line and band read as one instrument. */
.ch-wind{stroke:var(--link-ink);stroke-width:1.6;fill:none;
  stroke-linejoin:round;stroke-linecap:round}
.ch-gust{fill:rgba(21,83,110,.14)}   /* --link-ink, band opacity */
.ch-pt{fill:var(--link-ink)}
.ch-arrow{fill:var(--slate)}
.ch-now{stroke:var(--sunrise-deep);stroke-width:1.2;stroke-dasharray:3 3}
/* Swell vs wind sea: a blue/orange pair that clears the 3:1 non-text
   contrast bar on paper (--sunrise itself is area-only at 2.3:1, so the bar
   uses --sunrise-deep) and survives red-green color blindness; the in-chart
   legend pairs each fill with its word so color is never alone. */
.ch-swell{fill:var(--sea-bright)}
.ch-windsea{fill:var(--sunrise-deep)}
.ch-cap{stroke:var(--ink);stroke-width:1.4}
.ch-muted{opacity:.38}               /* suppressed day: muted, still drawn */
.ch-tide{stroke:var(--sea);stroke-width:1.6;fill:none}
.ch-dot{fill:var(--sea)}
.ch-flood{fill:var(--prime-ink)}     /* flood/ebb match the go/rough inks */
.ch-ebb{fill:var(--rough-ink)}
.ch-slack{fill:var(--paper);stroke:var(--slate-soft);stroke-width:1.2}

/* Live forecast "building" state (async loader) ------------------------- */
.live-building{display:flex;align-items:center;gap:12px;margin-top:18px;
  color:var(--slate-soft);font-size:.92rem}
.spinner{flex:0 0 auto;width:18px;height:18px;border-radius:50%;
  border:2px solid var(--line);border-top-color:var(--sea);
  animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
@media(prefers-reduced-motion:reduce){.spinner{animation:none}}

/* Map picker (settings) ------------------------------------------------- */
/* SIZED BY ITS OWN COLUMN, not by a fixed number. A flat height made the
   map a letterbox the moment it sat in a wide column - 1100px of frame,
   250px of water, most of it empty - and a flat height is the only way to
   get that wrong. aspect-ratio lets the box grow with the width it is given
   and the min/max keep it sane at both ends: never shorter than the old
   phone height, never taller than most of the viewport. */
.map-box{aspect-ratio:4/3;height:auto;min-height:340px;
  max-height:78vh;max-height:78svh;
  border:1px solid var(--line);
  border-radius:var(--radius-sm);overflow:hidden;position:relative;
  /* z-index:0 makes this a stacking CONTEXT: Leaflet's panes and controls
     run z-index 400-1000 INSIDE it, and without this they escape into the
     page's own stacking order - above the working veil, above the sticky
     header on scroll. Contained, they can outrank nothing outside the box. */
  z-index:0}
@media(min-width:768px){.map-box{aspect-ratio:16/10;min-height:440px}}
.map-fallback{padding:16px;color:var(--slate-soft);font-size:.9rem}
.leaflet-container{font-family:var(--font-body)}
.mk-zone{background:var(--sunrise);color:var(--ink);border:2px solid #fff;
  border-radius:50%;width:30px;height:30px;display:grid;place-items:center;
  font-family:var(--font-mono);font-weight:600;font-size:14px;
  box-shadow:0 2px 6px rgba(12,42,57,.4)}

/* The home port is deliberately NOT a numbered circle: it is not one of the
   spots, it is the point every plan runs out from and the point the run home
   is measured back to. Deep sea colour, an anchor, and it sits above the zone
   markers so a spot dropped on the same ramp cannot hide it. */
.mk-home{background:var(--sea);color:#fff;border:2px solid #fff;
  border-radius:50%;width:30px;height:30px;display:grid;place-items:center;
  font-size:16px;line-height:1;
  box-shadow:0 2px 8px rgba(12,42,57,.5)}

/* What a tap on the map means. Segmented, not a checkbox: the two states are
   equally legitimate and one of them is destructive-ish (a stray tap in the
   wrong mode moves the ramp every plan launches from). */
.map-mode{display:flex;gap:0;margin:0 0 10px;
  border:1px solid var(--line);border-radius:10px;overflow:hidden;
  width:fit-content;max-width:100%}
.map-mode-btn{appearance:none;background:var(--card);color:var(--ink);
  border:0;padding:10px 16px;min-height:44px;font:inherit;font-weight:600;
  cursor:pointer;white-space:nowrap}
.map-mode-btn + .map-mode-btn{border-left:1px solid var(--line)}
.map-mode-btn.is-on{background:var(--sea);color:#fff}
.map-mode-btn:focus-visible{outline:2px solid var(--sunrise);outline-offset:-2px}
/* Desktop only: on a touch screen there is no cursor to change, which is why
   the button state carries the meaning rather than this. */
.map-box.picking-home{cursor:crosshair}

/* Forecast-current arrows: the shared toggle control (currents.js). One
   glyph button in the standard Leaflet bar - it inherits the 44px tap floor
   set on .leaflet-bar a above - and .is-on flips it to the sea colour the
   way the mode buttons do, so on/off reads at a glance. */
.wf-curr a{font-weight:700;color:var(--ink);text-decoration:none;
  width:var(--tap);height:var(--tap);line-height:var(--tap)}
.wf-curr.is-on a{background:var(--sea);color:#fff}

/* Conditions map (/map) -------------------------------------------------- */
/* Full-bleed out of the .wrap gutters: the map is the page, and a reading
   column's worth of ocean is not enough water to plan a run on. This used to
   be calc(50% - 50vw), which overshoots by half the scrollbar width on a
   desktop and pushed the page into a horizontal scroll that only
   body{overflow-x:hidden} was hiding. Cancelling the gutter exactly is both
   simpler and scrollbar-safe. The padding-bottom is the home-indicator
   inset; map.js reads it back off this element (see fitMapHeight). */
.map-bleed{margin-left:calc(var(--gutter) * -1);
  margin-right:calc(var(--gutter) * -1);
  padding:0 clamp(8px,2vw,28px) env(safe-area-inset-bottom,0px)}
/* Height is set by map.js from the space actually left under the page head
   (fitMapHeight), so the whole map lands inside the window with no scrolling
   whatever the heading wraps to. This is the pre-JS floor; svh rather than
   vh so a phone's collapsing browser chrome cannot cut the bottom off. */
.map-view{height:70vh;height:70svh;min-height:360px;border:1px solid var(--line);
  border-radius:var(--radius-sm);overflow:hidden;position:relative}
/* Leaflet ships 30px controls. On a phone every one of them is a finger
   target, so they get the same 44px floor as everything else, and the
   expanded layer list is capped + scrolled so eight overlays cannot cover
   the map or reach down into the legend. */
.map-view .leaflet-bar a,
.map-view .leaflet-control-layers-toggle,
.map-box .leaflet-bar a{width:var(--tap);height:var(--tap);
  line-height:var(--tap);background-size:22px 22px}
.map-view .leaflet-control-layers-expanded{max-height:52svh;overflow-y:auto;
  padding:6px 12px 6px 8px}
/* display:flex on the label only - Leaflet's inner holder element differs by
   version, so nothing here reaches inside it. */
.map-view .leaflet-control-layers label{display:flex;align-items:center;
  min-height:var(--tap);margin:0}
/* Local tier: panning is off, so don't advertise a grab that does nothing */
.map-locked .leaflet-grab,
.map-locked .leaflet-dragging .leaflet-grab{cursor:default}
/* max-width in vw as well as px: on a 320px phone a flat 230px legend is
   most of the map, and it sits in the same bottom corner the attribution
   and the zoom-out control want. */
.map-legend{background:#fff;padding:8px 10px;border-radius:6px;
  box-shadow:0 1px 4px rgba(12,42,57,.3);font-size:.8rem;
  max-width:min(230px,62vw);color:var(--ink)}
.map-legend-head{font-weight:600}
.map-legend-bar{height:10px;border-radius:3px;margin:5px 0 2px}
.map-legend-minmax{display:flex;justify-content:space-between;
  font-family:var(--font-mono);font-size:.7rem;color:var(--slate)}
.map-legend-note{color:var(--slate-soft);margin-top:4px}
/* Hover reading: mono so the digits hold still as the cursor moves. */
.map-legend-read{font-family:var(--font-mono);font-size:.78rem;
  color:var(--ink);margin-top:5px;padding-top:5px;
  border-top:1px solid var(--line);min-height:1.1em}
.buoy-pop{font-size:.82rem;min-width:170px}
.buoy-pop-head{font-weight:600;margin-bottom:2px}
.buoy-pop-age{color:var(--slate-soft);font-size:.75rem;margin-bottom:4px}
.buoy-pop table{border-collapse:collapse}
.buoy-pop td{padding:1px 8px 1px 0;vertical-align:top}
.buoy-pop-upsell{margin:4px 0 6px}

/* Radar scrubber (/map) -------------------------------------------------- */
/* Forecast frames are model output that reads exactly like radar, so the
   amber (caution, not error) family marks the observed -> forecast boundary:
   FORECAST badge, amber thumb, tinted future track segment. */
.radar-scrub{margin-top:10px}
/* app.css gives buttons/inputs display rules that would defeat [hidden]. */
.radar-scrub [hidden]{display:none !important}
/* Wraps rather than crushing: Play + a usable slider + a timestamp + the
   FORECAST badge do not fit one 375px line, and a 40px-wide scrubber is not
   a scrubber. The timestamp takes its own row when it has to. */
.radar-controls{display:flex;align-items:center;flex-wrap:wrap;gap:8px 12px;
  margin-top:10px}
.radar-play{margin:0;padding:6px 16px;min-height:var(--tap);
  background:var(--mist);color:var(--sea);box-shadow:none;
  border:1px solid var(--line);font-size:.85rem}
.radar-play:hover{background:var(--seaglass-soft);transform:none}
/* The control is 44px tall so the whole strip is a tap target; the visible
   6px track is drawn by the pseudo-track inside it rather than by the
   element's own background, which would have been 6px of hit area. */
.radar-scrub input[type=range]{-webkit-appearance:none;appearance:none;
  flex:1 1 150px;width:auto;margin:0;padding:0;height:var(--tap);
  min-height:var(--tap);border:none;background:transparent}
.radar-scrub input[type=range]:focus{border:none;box-shadow:none}
.radar-scrub input[type=range]::-webkit-slider-runnable-track{height:6px;
  border-radius:3px;background:var(--line)}
.radar-scrub input[type=range]::-moz-range-track{height:6px;
  border-radius:3px;background:var(--line)}
.radar-scrub input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;
  width:18px;height:18px;margin-top:-6px;   /* centre on the 6px track */
  border-radius:50%;background:var(--sea);
  border:2px solid #fff;box-shadow:0 1px 3px rgba(12,42,57,.4);cursor:pointer}
.radar-scrub input[type=range]::-moz-range-thumb{width:18px;height:18px;
  border-radius:50%;background:var(--sea);border:2px solid #fff;
  box-shadow:0 1px 3px rgba(12,42,57,.4);cursor:pointer}
.radar-scrub.is-forecast input[type=range]::-webkit-slider-thumb{
  background:var(--sunrise-deep)}
.radar-scrub.is-forecast input[type=range]::-moz-range-thumb{
  background:var(--sunrise-deep)}
.radar-time{display:inline-flex;align-items:center;gap:8px;
  font-family:var(--font-mono);font-size:12px;color:var(--slate);
  white-space:nowrap}
/* Below this the slider would be squeezed to nothing by the timestamp, so
   the timestamp claims a whole row instead. */
@media(max-width:520px){.radar-time{flex:1 0 100%}}
.radar-forecast-badge{background:var(--sunrise-soft);color:var(--fair-ink);
  border:1px solid rgba(232,154,69,.5);border-radius:var(--radius-pill);
  font-family:var(--font-mono);font-size:10.5px;font-weight:600;
  letter-spacing:.12em;padding:2px 9px}
.radar-stale{margin:10px 0 0}

/* ---- The fishing plan (webapp/planview.py + templates/plan.html) --------
   Read at a boat ramp at 5am, one-handed, in sunlight. Three consequences:

   - The card is plan.render() output, a fixed 72-column monospace block whose
     LABEL / body alignment is part of how it reads. Squeezing it to a phone
     would either break that alignment or push the type under the 10.5px
     floor, so it PANS sideways instead - the same answer .chart-scroll and
     .table-scroll already give wide content.
   - Maximum contrast: --ink on --cloud, no tinted body text anywhere in the
     card. A washed-out grey is unreadable on a phone in direct sun.
   - Nothing in here colors the card by verdict. The words carry the verdict;
     a green panel would be an all-clear the language rules forbid, and a red
     one would rank a refusal as a failure when it is a complete answer. The
     refusal gets a neutral weight change (a heavier left rule), not a color. */
.plan-entry{border-left:3px solid var(--sea)}

.plan-controls{display:flex;flex-wrap:wrap;gap:0 14px;align-items:flex-end;
  margin:10px 0 4px;max-width:var(--measure-form)}
.plan-controls label{margin:8px 0;flex:1 1 190px;min-width:0}
.plan-controls button{margin-top:8px;flex:0 0 auto}
.plan-tier-note{margin:6px 0 0}

/* width:fit-content + max-width:100% is the whole adaptive trick here. The
   card is a fixed 72 columns, so the box shrink-wraps it once there is room
   (no acre of empty white beside it on a 1440px display) and falls back to
   the container - and therefore to panning - when there is not. It is also
   what guarantees this thing can never widen the PAGE: the box is at most
   100% of its column, whatever the text inside it measures.
   background-color, not background, so the shared pan-shadow gradients
   above survive. */
.plan-scroll{width:fit-content;max-width:100%;
  margin-top:12px;background-color:var(--cloud);border:1px solid var(--line);
  border-left:3px solid var(--seaglass);
  border-radius:var(--radius-sm);box-shadow:var(--shadow-card);
  --scroll-cover:var(--cloud)}
/* A refusal is a first-class output, so it is weighted, not colored. */
.plan-scroll-refusal{border-left-width:5px;border-left-color:var(--slate-soft)}
.plan-card{margin:0;padding:14px 16px;color:var(--ink);
  font-family:var(--font-mono);
  /* Fluid, with a hard floor and a sane ceiling. 12px is the floor, not the
     10.5px type floor: this is 72 columns of decision text read one-handed
     at a boat ramp at 5am, and dropping it further to buy a phone a few
     characters would trade legibility for a scroll we are happy to keep.
     72 columns will not fit a 375px screen at ANY legible size, and
     pretending otherwise is what the sideways pan is for. Above ~1030px the
     whole card fits and the type keeps growing with the window until 16px,
     where a monospace block stops reading as a card and starts reading as a
     wall - that ceiling is about type, not about locking the layout. */
  font-size:clamp(12px,1.55vw,16px);line-height:1.55;
  white-space:pre;         /* the alignment is load-bearing - never pre-wrap */
  tab-size:4}
@media(min-width:768px){.plan-card{padding:18px 20px}}

.plan-feedback{margin-top:18px;border-left:3px solid var(--sunrise)}
.plan-feedback fieldset{margin:12px 0 0}
.plan-feedback legend{padding:0;margin-bottom:7px}
.plan-feedback textarea{min-height:84px;resize:vertical}
/* The radio stays VISIBLE, sized like the existing chip checkboxes rather
   than hidden behind :has(). Hiding it would leave a browser without :has
   support showing no selected state at all, and the one thing this control
   must never be is ambiguous about which answer was given. Without this the
   global input{width:100%} rule blows the radio up to the chip's width. */
.plan-feedback .chip input[type=radio]{width:auto;min-height:0;
  margin:0 9px 0 0;flex:0 0 auto}

.outcome-table{min-width:1100px}
.outcome-table td{text-align:left;white-space:nowrap}
.outcome-table td.wide{white-space:normal;min-width:200px;
  overflow-wrap:anywhere}

/* ---- The two directions of the product ---------------------------------
   MY SPOTS (place -> species, spot_report.py) and MY SPECIES (species ->
   places, species_locations.py) are peers, so they look like peers: the same
   type scale, the same weights, no colour separating them. Everything below
   is prose and evidence rather than instruments, so unlike the plan card it
   REFLOWS - there is no fixed column to preserve, and a captain reading why
   a place was picked wants wrapped sentences, not a sideways pan.

   Nothing in here is coloured by outcome. A refusal gets a heavier left rule
   (the same weight-not-colour treatment .plan-scroll-refusal gets); an
   unknown reading gets a dashed rule, because a green "all clear" and a red
   "bad" are both verdicts this product does not render in CSS. */

/* The two-question switch. Compact pills sized to their own words (the
   min-height keeps the 44px tap target on a phone). Never a dropdown: both
   questions have to be visible at once or one of them is a feature nobody
   finds. */
.plan-tabs{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0 2px}
.plan-tabs a{display:inline-flex;align-items:center;justify-content:center;
  flex:0 1 auto;min-height:var(--tap);padding:6px 22px;
  background:var(--mist);border:1px solid var(--line);
  border-radius:var(--radius-pill);color:var(--link-ink);
  font-family:var(--font-mono);font-size:12px;letter-spacing:.06em;
  text-decoration:none}
.plan-tabs a:hover{background:var(--seaglass-soft)}
.plan-tabs a[aria-current=page]{background:var(--sunrise-soft);
  border-color:rgba(232,154,69,.5);color:var(--fair-ink);font-weight:600}
.plan-tabs a:focus-visible{outline:2px solid var(--sea);outline-offset:2px}

/* ---- the week strip (Pro lookahead at a glance) ------------------------ */
/* One chip per horizon day, each a link into its day. STATE IS NEVER COLOUR
   ALONE: every chip carries its state word, and the three states also differ
   in border TREATMENT - solid for an answered day, dashed for one we cannot
   see - so the strip survives monochrome and colour-blindness alike. The
   unknown chip is deliberately the visually quietest: a day we cannot vouch
   for must not out-shout one we refused. */
.week-strip{margin:14px 0 4px}
.week-strip .lbl{font-family:var(--font-mono);font-size:11px;
  letter-spacing:.14em;text-transform:uppercase;color:var(--sea);
  margin:0 0 8px}
.strip-days{list-style:none;display:flex;flex-wrap:wrap;gap:8px;
  margin:0;padding:0}
.strip-chip{display:flex;flex-direction:column;align-items:flex-start;
  gap:1px;min-height:var(--tap);min-width:64px;padding:6px 10px;
  background:var(--mist);border:1px solid var(--line);
  border-radius:var(--radius-sm);color:var(--ink);text-decoration:none}
.strip-chip:hover{background:var(--seaglass-soft)}
.strip-chip:focus-visible{outline:2px solid var(--sea);outline-offset:2px}
.strip-chip[aria-current=page]{background:var(--sunrise-soft);
  border-color:rgba(232,154,69,.5)}
.strip-date{font-family:var(--font-mono);font-size:11px;
  letter-spacing:.06em;color:var(--slate-soft)}
.strip-state{font-size:.85rem;font-weight:600}
.strip-plan{border-left:3px solid var(--prime)}
.strip-plan .strip-state{color:var(--prime-ink)}
.strip-refused{border-left:5px solid var(--slate-soft)}
.strip-refused .strip-state{color:var(--rough-ink)}
.strip-unknown{border-style:dashed}
.strip-unknown .strip-state{color:var(--slate-soft);font-weight:400}
/* An advisory day: the chip wears the banner's CONDITIONS family - amber
   fill, heavy rule - and its state word, never colour alone. The day still
   builds; the chip points at a card wearing a stated advisory. */
.strip-advisory{border-left:5px solid var(--sunrise-deep);
  background:var(--sunrise-soft)}
.strip-advisory .strip-state{color:var(--fair-ink)}
/* The typed reason on a refused or unknown chip: code vocabulary drawn as
   code, small and mono, never dressed up as a sentence. */
.strip-reason{font-family:var(--font-mono);font-size:10.5px;
  color:var(--slate-soft)}

/* ---- the typeset plan card -------------------------------------------- */
/* The answer, set in the page's own type instead of dumped as a monospace
   blob. Same words - plan.view_model and plan.render read one object - so
   this is layout only. A LINE never stretches past prose width, but the
   PAGE uses the whole screen: on a wide display the sections flow into
   newspaper columns (see .plan-secs below) instead of scrolling one narrow
   measure past a field of dead space. */
.plan-typeset{max-width:72ch;margin:14px 0 6px}
.plan-title{font-family:var(--font-mono);font-size:13px;font-weight:600;
  letter-spacing:.08em;color:var(--sea);margin:0 0 10px;
  max-width:none}/* the generic h2 26ch cap wraps this one-liner at ~200px */
.plan-headline{font-family:var(--font-display);font-size:clamp(19px,2.6vw,24px);
  line-height:1.35;margin:0 0 6px;color:var(--ink)}
.plan-drops{margin:0 0 18px}
.plan-sec{margin:0 0 16px}
.plan-sec-label{font-family:var(--font-mono);font-size:11px;font-weight:600;
  letter-spacing:.14em;color:var(--slate);margin:0 0 4px}
/* A label-less section (the refusal's unranked line) keeps its h3 for grid
   placement; without this the generic h3 underline draws a stray rule, and
   its inherited padding/margin left ~10px of dead space in the stacked
   (sub-900px) layout. */
.plan-sec-label:empty{border-bottom:none;padding-bottom:0;margin:0}
.plan-sec-body p{margin:0 0 8px}
.plan-sec-body p:last-child{margin-bottom:0}
.plan-sec-body ol{margin:0;padding-left:22px}
.plan-sec-body ol li{margin:0 0 6px}
/* Label in the left margin once there is room - the monospace card's column
   layout, kept, but set in type. */
@media(min-width:900px){
  .plan-sec{display:grid;grid-template-columns:110px 1fr;gap:0 18px}
  .plan-sec-label{margin:3px 0 0;text-align:left}
}
/* THE WIDE SHEET, AND IT IS WIDTH-DRIVEN, NOT BREAKPOINT-DRIVEN. A single
   72ch column leaves most of a monitor empty, so the sections flow into as
   many readable columns as FIT: one on a phone, two on a 13-inch window,
   three or four on a desktop. column-width (not column-count, and not a
   media query) is what makes that adaptive - the browser divides whatever
   width it actually has by the measure, so this works at every viewport
   including the ones nobody thought to write a breakpoint for. Each column
   IS the measure, so lines never stretch however wide the screen gets, and
   the sheet's height drops by the same factor.

   This used to sit inside a min-width:1150px query, which meant a laptop at
   1000-1149px - or a browser at a zoom level that lands there - got the one
   narrow column and the whole field of dead space beside it. There is no
   width at which columns-that-fit is the wrong answer, so there is no
   breakpoint. */
.plan-typeset{max-width:none}
.plan-headline{max-width:56ch}
.plan-secs{column-width:27rem;column-gap:44px;
  column-rule:1px solid var(--line);margin:0 0 6px}
/* Inside a column the label goes above its text: a side label would spend a
   third of every column on margin. The single-column case is unaffected -
   prose still carries the page's own 72ch cap (see `main p`). */
.plan-secs .plan-sec{display:block;
  /* a column break straight after a label orphans it; a break inside a
     short section reads as two sections. Long bodies (HOW) still break -
     avoid is a request, not a wall, and an unbreakable HOW would be one
     screen-tall column. */
  -webkit-column-break-inside:avoid;break-inside:avoid-column}
.plan-secs .plan-sec:has(> .plan-sec-body > p:nth-child(4)){break-inside:auto}
.plan-secs .plan-sec-label{break-after:avoid;margin:0 0 4px}
/* The per-spot and per-fish blocks get the same sheet - BUT AS A GRID, not
   as columns, and that is a deliberate retreat from a primitive that kept
   getting this wrong.

   These blocks are independent answers, one per fish or per spot, and every
   property multi-column brings to them is a liability: it BALANCES (so the
   height of one tall block decides the shape of the whole set), it
   FRAGMENTS (so a spot report can split mid-verdict and read as two spots,
   which `break-inside` then has to fight), and a single unbreakable token
   inside a column widens every column at once - measured at 1,951px of
   content in a 1,900px window, which `body{overflow-x:hidden}` then clips
   rather than shows. The page rendered right and then snapped to one long
   left column, repeatedly, and the honest reading is that this is not what
   multi-column is for.

   auto-fill + minmax(min(100%,27rem)) is the same guard `.reads` carries a
   few rules down: as many readable columns as FIT, and a track that can
   never exceed the container, so no child can widen the page. Each article
   is one cell - it cannot split, cannot balance, and cannot move another
   block. Reading order is left to right, which is also the DOM order and
   the tab order. */
.card-flow{display:grid;align-items:start;gap:0 44px;
  grid-template-columns:repeat(auto-fill,minmax(min(100%,27rem),1fr))}
/* min-width:0 is the other half of the blowout guard: a grid item's default
   min-width is its min-content, which a long unbroken string can make wider
   than its own track. */
.card-flow>article{min-width:0;overflow-wrap:anywhere}
/* The plan card keeps its columns - its sections are one continuous answer
   of wildly uneven length, which is what newspaper flow is actually for -
   but it gets the same protection from a long token widening the sheet. */
.plan-secs .plan-sec-body{overflow-wrap:anywhere}
/* Closing paragraphs carry no position-based emphasis by default: the
   suppressed day's whole body and the not_known drops tally both arrive as
   closings, and a bold-first/muted-rest cascade bolded a tally on one and
   demoted gale-warning lines on the other. Only known_bad's closing is the
   [verdict, tally] pair the emphasis was designed for. */
.plan-closing{margin:14px 0 0}
.plan-refusal-known_bad .plan-closing{font-weight:600;color:var(--ink)}
.plan-refusal-known_bad .plan-closing + .plan-closing{font-weight:400;
  color:var(--slate)}
.plan-refusal-not_known .plan-closing{color:var(--slate)}

/* Refusals keep the weight-not-colour rule: a left rule, heavier for the
   suppressed day, and the same type as the plan - a refusal is a complete
   answer, not an error state. Weight means WIDTH here: no verdict colour on
   the rule and none on the title, matching the plan-card doctrine above and
   the neutral .plan-scroll-refusal copy of the same answer. */
.plan-refusal{border-left:4px solid var(--slate-soft);padding-left:16px}
.plan-refusal-suppressed{border-left-width:6px}

/* The monospace card survives under a disclosure as the audit/copy artefact. */
.plan-plain{margin:10px 0 4px}
.plan-plain>summary{cursor:pointer;color:var(--sea);font-weight:600;
  font-size:14px}
.plan-plain[open]>summary{margin-bottom:8px}

/* A later fish repeating an earlier refusal: one line, not the card again. */
.plan-same-refusal{margin:2px 0 6px}

/* ---- the advisory banner (the advisory pivot, config.ADVISORY_MODE) ----
   THE ONE DELIBERATE EXCEPTION to the weight-not-colour rule, and it bends
   in the CAUTIONING direction only: an advisory is the product's own no-go
   read carried ON a built answer, and it must out-shout everything under it
   without deleting any of it. Full width, heavy rule, colour ALWAYS paired
   with the header words (never colour alone). Two severities: OVERRIDE (a
   hard NWS / red-tide override - alarm ink) outranks CONDITIONS (over your
   own limits - the amber family). Shared verbatim by the plan page, the
   spot reports and the dashboard verdict area, so one read cannot dress
   differently on two surfaces. */
.advisory-banner{margin:14px 0;padding:14px 18px;
  border:1px solid var(--line);border-left:8px solid var(--sunrise-deep);
  border-radius:var(--radius-sm);background:var(--sunrise-soft)}
.advisory-eyebrow{display:block;font-family:var(--font-mono);font-size:11px;
  font-weight:600;letter-spacing:.14em;text-transform:uppercase;
  color:var(--fair-ink)}
.advisory-head{margin:4px 0 8px;font-family:var(--font-display);
  font-size:clamp(19px,2.6vw,26px);line-height:1.25;font-weight:600;
  color:var(--fair-ink);max-width:none}
.advisory-line{margin:6px 0 0;max-width:var(--measure);color:var(--ink)}
.advisory-margin{font-family:var(--font-mono);font-size:.9rem;
  font-weight:600}
.advisory-flip,.advisory-stands{margin:6px 0 0}
.advisory-call{margin:10px 0 0;font-weight:600;color:var(--ink);
  max-width:var(--measure)}
.advisory-conditions{border-left-color:var(--sunrise-deep);
  background:var(--sunrise-soft)}
.advisory-conditions .advisory-eyebrow,
.advisory-conditions .advisory-head{color:var(--fair-ink)}
.advisory-override{border-left-color:var(--rough);
  background:rgba(194,84,62,.10)}
.advisory-override .advisory-eyebrow,
.advisory-override .advisory-head{color:var(--rough-ink)}
/* Inside a spot block the banner leads the report it is a verdict on. */
.advisory-spot{margin-top:10px}

/* The per-place advisory chip: the state word + the kit's hard-override
   triangle, in the banner's conditions family. Never colour alone - the
   word rides every chip - and the mark has a legend entry (glyph key). */
.adv-chip{display:inline-flex;align-items:center;gap:5px;
  padding:2px 10px;vertical-align:2px;
  border:1px solid var(--sunrise-deep);border-radius:var(--radius-pill);
  background:var(--sunrise-soft);color:var(--fair-ink);
  font-family:var(--font-mono);font-size:11px;font-weight:600;
  letter-spacing:.08em;text-transform:uppercase;white-space:nowrap}
.adv-chip .gl{width:13px;height:13px;color:var(--rough-ink);flex:none}
.lg .adv-chip{flex:none}
/* The gate's own evidence sentence under a chipped place. */
.adv-note{font-size:.85rem;color:var(--fair-ink);max-width:var(--measure)}
/* A chipped place keeps the amber family on its left rule too - colour
   paired with the chip's word, never carrying the message alone. */
.loc-advisory{border-left-color:var(--sunrise-deep)}
/* The plan card's own ADVISORY section: the verdict leads the card and is
   weighted like the banner, not like a labelled body section. */
.plan-sec-advisory{padding:10px 14px;border-left:6px solid var(--sunrise-deep);
  background:var(--sunrise-soft);border-radius:var(--radius-sm)}
.plan-sec-advisory .plan-sec-label{color:var(--fair-ink)}
@media(min-width:900px){
  /* The grid variant would put ADVISORY's label in the margin like WHERE's;
     the verdict block keeps its stacked, banner-like shape instead. */
  .plan-sec.plan-sec-advisory{display:block}
}

/* The reading of the water at one place. auto-fill + minmax(min(100%,148px))
   is what keeps this off the page's horizontal scrollbar: below ~340px the
   min() collapses the track to the container width and the grid becomes one
   column, so a long unknown-reason string can never widen the page. */
.reads{display:grid;gap:8px;margin:10px 0 0;
  grid-template-columns:repeat(auto-fill,minmax(min(100%,148px),1fr))}
.read{background:var(--cloud);border:1px solid var(--line);
  border-left:3px solid var(--seaglass);border-radius:var(--radius-sm);
  padding:9px 11px;min-width:0}
.read .lbl{display:block;font-family:var(--font-mono);font-size:10.5px;
  letter-spacing:.1em;text-transform:uppercase;color:var(--slate-soft)}
.read .val{display:block;font-family:var(--font-mono);font-size:1rem;
  color:var(--ink);overflow-wrap:anywhere}
.read .sub{display:block;font-size:.78rem;color:var(--slate-soft);
  line-height:1.45;overflow-wrap:anywhere}
/* An unknown is a different ANSWER, not a missing one - dashed, and its
   reason is printed under it rather than left blank. */
.read-unknown,.read-withheld{border-left-style:dashed;background:var(--mist)}
.read-unknown .val,.read-withheld .val{color:var(--slate)}

/* One saved spot's report, and one species' places. Same shell on purpose. */
.spot-report,.species-locations{margin:22px 0 0;padding-top:14px;
  border-top:1px solid var(--line)}
.spot-report h2,.species-locations h3{margin:0 0 2px}
.loc-head{margin:16px 0 0;font-family:var(--font-mono);font-size:11px;
  letter-spacing:.1em;text-transform:uppercase;color:var(--slate-soft);
  font-weight:500}
/* Weight, never colour - a refusal is a complete answer, not a failure. */
.spot-refusal{border-left-width:5px;border-left-color:var(--slate-soft)}

/* One species guessed at a spot, and one place put forward for a species. */
.guess,.loc{margin:12px 0 0;padding:0 0 0 14px;
  border-left:2px solid var(--seaglass)}
.guess h4,.loc h5{margin:0 0 2px;font-family:var(--font-display);
  font-size:1.02rem;font-weight:500;color:var(--ink)}
.guess p,.loc p{margin:4px 0 0;max-width:var(--measure)}
.loc-where{font-family:var(--font-mono);font-size:.82rem;color:var(--slate)}
/* KB technique prose. Indented and quiet: it is the record talking, and its
   attribution line rides directly under it so the two never come apart. */
.tactic{margin:10px 0 0;padding:9px 12px;background:var(--mist);
  border-radius:var(--radius-sm)}
.tactic p{margin:0;max-width:var(--measure)}
.tactic p.sub{margin-top:4px;font-size:.78rem}
.spot-limits{margin-top:12px}
.spot-tides,.spot-windows{font-family:var(--font-mono);font-size:.8rem;
  overflow-wrap:anywhere}
/* THE HEADLINE OF A SPECIES BLOCK, and it is OUTSIDE the disclosure on
   purpose. The plan renders every species this tier allows at once, so each
   one has to say something true while they are all still collapsed - an
   unknown and a refusal are complete answers here, not empty states, and both
   land in this paragraph at the same weight a list of places does. */
.species-headline{margin:6px 0 0;max-width:var(--measure);color:var(--slate)}
/* The disclosure that carries the places, the figure and the tactics. Native
   details/summary: no script, no CSP allowance, keyboard-reachable, prints. */
.species-block{margin-top:10px}
.species-block[open]>summary{margin-bottom:4px}

/* WHERE THESE PLACES LIE - the single species map (planmap.js). One map,
   one toggle chip per species; the disclaimer is the figcaption and every
   pin popup carries its own position caveat. The map never joins the
   .plan-secs column flow: a viewport-wide element inside CSS columns would
   be clipped to one column. */
.species-map-wrap{margin:12px 0 20px}
/* Sized like .map-box: aspect-ratio with a PX floor, never a bare vh.
   A vh height is 0 in any context that reports a zero viewport (observed
   in the embedded preview pane), and a map initialised at 0px keeps a
   world-zoom view even after the box grows. The px floor guarantees
   Leaflet always measures something real. */
.species-map{aspect-ratio:16/10;height:auto;min-height:340px;
  max-height:78vh;max-height:78svh;
  border:1px solid var(--line);
  border-radius:var(--radius-sm);overflow:hidden;
  /* position+z-index:0 = a stacking CONTEXT, same as .map-box: Leaflet's
     panes run z-index 400-1000 inside it and must not escape over the
     sticky header or the working veil. */
  position:relative;z-index:0}
.sp-chips{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0}
.sp-chip{display:inline-flex;align-items:center;gap:7px;
  min-height:var(--tap);padding:4px 14px;border:1px solid var(--line);
  border-radius:999px;background:var(--card,#fff);cursor:pointer;
  font:inherit;color:inherit}
.sp-chip.is-off{opacity:.45}
.sp-chip.is-off .sp-chip-dot{background:var(--slate-soft)!important}
.sp-chip-dot{width:10px;height:10px;border-radius:50%;flex:none}
/* Legends: every symbol that repeats, shown once beside its meaning. */
.maplegend{display:flex;flex-wrap:wrap;gap:6px 16px;margin:8px 0 2px;
  font-size:.82rem;color:var(--slate-soft)}
/* The map legend's rows are SENTENCES, not the two-word labels .lg was drawn
   for - "Outlined water: the sheltered water this fish holds in" is 53
   characters, and nowrap made it wider than a phone. It wraps here like the
   glyph key's rows do; the swatch is flex:none, so it stays on the first line
   with the words it names. */
.maplegend .lg{white-space:normal;max-width:100%}
.lg{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
.lg .gl{width:22px;height:14px}
.lg .gl-fit{width:22px;height:22px}
.lg .gl-season{width:80px;height:12px}
.lg-dot{width:12px;height:12px;border-radius:50%;background:var(--sea);
  border:2px solid #fff;box-shadow:0 0 0 1px rgba(23,42,58,.25);flex:none}
.lg-dot-soft{opacity:.55}
.lg-dot-hollow{background:#fff;border:2px solid var(--sea);opacity:.8}
/* The FIELD mark: every other place that suits the fish, drawn small on a
   canvas rather than as a pin. Same dot at a mark's size, because the three
   partition treatments above are the ones the marks use too - one grammar
   for both tiers, so this row names the size and the rows above name the
   ink. */
.lg-dot-field{width:7px;height:7px;border-width:1px}
/* WHERE THE ANSWER LEADS: the same dot, fatter, inside the gold the glyph
   kit already uses for the fit. Only drawn on a page that has a ranked
   place to lead with - see the payload's best_key. The ring is a box-shadow,
   which takes no layout, so without the margin the first row's ring is
   clipped by the legend's own left edge. */
.lg-dot-best{width:14px;height:14px;margin:0 4px;
  box-shadow:0 0 0 3px var(--sunrise),0 0 0 4px rgba(12,42,57,.35)}
.lg-home{width:12px;height:12px;border-radius:50%;background:#fff;
  border:3px solid var(--slate);box-sizing:border-box;flex:none}
/* The searched radius. A casing under an ink, matching the map's own ring:
   one hairline was invisible against the tints, the survey fill and the
   field marks, and the swatch has to name what is actually drawn. Long
   dashes and the white casing are what keep it from reading as the survey's
   short-dashed slate outline. */
.lg-ring{width:16px;height:16px;border-radius:50%;
  border:2px dashed #15536E;box-shadow:0 0 0 1.5px rgba(255,255,255,.9);
  flex:none}
.lg-arrow{font-weight:700;color:var(--sea)}
/* The depth-band swatch: the species' chip colour arrives inline; shown a
   shade stronger than the map's 0.22 tint so it survives a white page. */
.lg-band{width:18px;height:12px;border:1px solid var(--line);
  border-radius:3px;opacity:.35;flex:none}
/* ...and the enclosed-water SURVEY, which is a different claim from the
   tint: the tint is our reading of the record over a depth grid, this is a
   survey somebody drew by hand. It is now filled AS WELL AS outlined on the
   map - inside the survey the depth tint is nulled, and a hairline was the
   only inshore highlight the owner ever got - so the swatch is filled too,
   and it is the heavy border rather than an empty middle that separates it
   from a tint's swatch. Both colours arrive inline, from the one ink. */
.lg-band-outline{border-width:2px;opacity:.75;background:transparent}
.lg-chip{width:18px;height:12px;border:1px solid var(--line);
  border-radius:4px;background:var(--cloud);flex:none}
.lg-chip-dashed{border-style:dashed}
.rchips-key{margin-top:2px}
.glyphkey{margin:8px 0 14px}
.glyphkey summary{min-height:var(--tap);display:flex;align-items:center;
  font-family:var(--font-mono);font-size:11.5px;letter-spacing:.06em;
  color:var(--link-ink);cursor:pointer}
.glyphkey-grid{display:flex;flex-wrap:wrap;gap:8px 22px;padding:4px 0;
  font-size:.85rem;color:var(--slate)}
.glyphkey-grid .lg{white-space:normal;max-width:280px}

/* The plan map's current badge: an arrow rotated TOWARD + knots. */
.spm-curr{display:flex;align-items:center;gap:2px;
  font-family:var(--font-mono);font-size:10px;color:var(--ink);
  text-shadow:0 0 3px #fff,0 0 3px #fff;pointer-events:none}
.spm-curr-arrow{display:inline-block;font-weight:700;color:var(--sea)}
.spm-curr-kt{font-weight:600}

/* Where you launch: a ringed dot, distinct from every species pin. */
.spm-home{width:18px;height:18px;border-radius:50%;
  border:3px solid var(--slate);background:#fff;box-sizing:border-box}
/* One place. The color arrives inline from the species' chip; the soft
   variant is the unranked/unread partition - same dot, lower confidence. */
.spm-dot{width:16px;height:16px;border-radius:50%;
  border:2px solid #fff;box-sizing:border-box;
  box-shadow:0 0 0 1px rgba(23,42,58,.25)}
.spm-dot--soft{opacity:.55}
/* WHERE THIS FISH'S ANSWER LEADS on the plan's day (planview._best_picks).
   Bigger AND ringed, never colour alone - and the ring is gold because gold
   is already the fit's ink in the glyph kit, so the mark says "highest fit"
   in a grammar the page has been speaking since the fit ring. The species'
   own colour still fills it, so the pin never stops saying whose it is. */
.spm-dot--best{width:22px;height:22px;border-width:3px;
  box-shadow:0 0 0 3px var(--sunrise),0 0 0 4px rgba(12,42,57,.4)}
/* A row below the map was tapped and this is its pin. Two beats, then gone:
   the mark is an ANSWER TO A TAP, not a state, and a highlight that stayed
   would still be claiming something two taps later. */
@keyframes spm-found{
  0%{box-shadow:0 0 0 3px var(--sunrise),0 0 0 16px rgba(232,154,69,0)}
  30%{box-shadow:0 0 0 4px var(--sunrise),0 0 0 14px rgba(232,154,69,.45)}
  100%{box-shadow:0 0 0 3px var(--sunrise),0 0 0 16px rgba(232,154,69,0)}}
.spm-found .spm-dot{animation:spm-found 1.9s ease-out 2}
@media (prefers-reduced-motion:reduce){
  .spm-found .spm-dot{animation:none;
    box-shadow:0 0 0 4px var(--sunrise),0 0 0 7px rgba(12,42,57,.4)}}
/* The list row that points at it. A NAME, not a button-looking thing: the
   row is prose and the affordance is the underline plus the pointer, the
   same weight a link carries in this body copy. planmap.js adds the class
   only for a place it found a pin for, so a name that is not underlined is
   a name with nothing to show. */
.loc-name.is-locatable{cursor:pointer;
  text-decoration:underline;text-decoration-style:dotted;
  text-underline-offset:3px;text-decoration-color:var(--slate-soft)}
.loc-name.is-locatable:hover,.loc-name.is-locatable:focus-visible{
  text-decoration-style:solid;text-decoration-color:var(--sunrise-deep)}
.loc-name.is-locatable:focus-visible{outline:2px solid var(--sunrise-deep);
  outline-offset:2px;border-radius:3px}
/* ---- The glyph kit (webapp/glyphs.py) --------------------------------
   Server-rendered inline SVG; colors come from the page tokens via
   currentColor and the classes below, so the drawings theme with the
   site. NO TRAFFIC LIGHTS: facts encode as position and fill in the sea
   palette; gold marks the fit - the separate fourth number - and alarm
   ink belongs only to the hard-override icon. */
.gl{display:inline-block;vertical-align:middle}
.gl-icon{width:18px;height:18px;flex:none}

/* The reads as chips. NAMESPACED rchip, not chip: the settings species
   picker owned .chips/.chip first (the flex pills above), and an unprefixed
   second meaning restyled that picker from another page's stylesheet - the
   exact bug class one shared sheet invites. */
.rchips{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));
  gap:8px;margin:10px 0 4px}
.rchip{display:flex;align-items:center;gap:8px;padding:7px 10px;
  border:1px solid var(--line);border-radius:var(--radius-sm);
  background:var(--cloud);color:var(--slate);min-width:0}
.rchip-body{display:flex;flex-direction:column;min-width:0}
.rchip-lbl{font-family:var(--font-mono);font-size:10.5px;
  letter-spacing:.05em;text-transform:uppercase;color:var(--slate-soft)}
.rchip-val{font-size:.92rem;color:var(--ink);overflow-wrap:anywhere}
/* Unknown and withheld are DRAWN states: muted ink, dashed frame - never
   an absent chip, never a filled shape at zero. */
.rchip-unknown,.rchip-withheld{border-style:dashed;color:var(--slate-soft)}
.rchip-unknown .rchip-val,.rchip-withheld .rchip-val{color:var(--slate-soft)}
.rchip-stale .rchip-val{text-decoration:underline dotted}
.rchip .gl-red_tide{color:var(--rough-ink)}
.reads-why{margin:2px 0 8px}
.reads-why summary{min-height:var(--tap);display:flex;align-items:center;
  font-family:var(--font-mono);font-size:11.5px;letter-spacing:.06em;
  color:var(--link-ink);cursor:pointer}
.reads-why ul{margin:0 0 0 18px;padding:0}
.reads-why li{margin:3px 0;max-width:var(--measure)}

/* The fit ring - gold, the fourth number's own ink. */
.gl-fit{width:40px;height:40px;flex:none}
.fitr-track{fill:none;stroke:var(--line);stroke-width:3.5}
.fitr-arc{fill:none;stroke:var(--sunrise-deep);stroke-width:3.5;
  stroke-linecap:round}
.fitr-num{fill:var(--ink);font-family:var(--font-mono);font-size:13px;
  font-weight:600}
.fitr-num-unknown{fill:var(--slate-soft)}
.fitr-unknown{stroke:var(--slate-soft)}

/* The season strip. */
.season-row{display:flex;align-items:center;gap:8px;margin:6px 0 2px}
.glyph-cap{font-family:var(--font-mono);font-size:10.5px;
  letter-spacing:.05em;text-transform:uppercase;color:var(--slate-soft)}
.gl-season{width:170px;height:18px;display:block}
.sea-mo{fill:var(--slate-soft);font-family:var(--font-mono);font-size:8px}
.sea-mo-on{fill:var(--ink);font-weight:600}
.sea-peak + .sea-mo-on{fill:var(--cloud)}
.sea-off{fill:none;stroke:var(--line);stroke-width:1}
.sea-on{fill:var(--seaglass);stroke:var(--sea);stroke-width:.5}
.sea-peak{fill:var(--sea);stroke:var(--sea);stroke-width:.5}
.sea-now{fill:none;stroke:var(--sunrise-deep);stroke-width:1.5}

/* The depth bar. */
.gl-depth{width:112px;height:18px;flex:none}
.db-axis{stroke:var(--line);stroke-width:1.2}
.db-tick{stroke:var(--line);stroke-width:1}
.db-band{fill:var(--seaglass);stroke:var(--sea);stroke-width:.6}
.db-marker{fill:var(--ink)}

/* Water pictograms + the current arrow ride the slate ink. */
.gl-water,.gl-curr{width:18px;height:18px;color:var(--sea);flex:none}
.gl-water-unknown,.gl-curr-unknown{color:var(--slate-soft)}
.glv{font-family:var(--font-mono);font-size:11.5px;color:var(--slate)}

/* The location glyph header. */
.loc-top{display:flex;align-items:center;justify-content:space-between;
  gap:8px}
.loc-top h5{margin:0}
.loc-glyphs{display:flex;align-items:center;gap:8px;margin:4px 0 2px}

/* Tactic category icon rides inline before the card's first words. */
.tactic .gl-tactic{margin-right:6px;color:var(--sea);
  vertical-align:-3px}

/* Inline depth labels on the major isobaths (contours.js). A white halo
   keeps the figure legible over any base-map ink; pointer-events off so a
   label never eats a tap meant for the line under it. */
.ct-label{font-family:var(--font-mono);font-size:10.5px;font-weight:600;
  color:#2a4d68;white-space:nowrap;pointer-events:none;
  text-shadow:0 0 3px #fff,0 0 3px #fff,0 0 3px #fff;
  transform:translate(-50%,-50%)}
/* Unplaceable: water we could not read. Hollow - a colored ring around
   white - so it cannot read as a spot at a glance; the figcaption's pin
   key names all three states. */
.spm-dot--unread{background:#fff;border:2px solid;opacity:.8}
.spm-pop b{display:block;margin-bottom:2px}
.spm-pop p{margin:4px 0 0;max-width:34ch}

/* details/summary is the disclosure for every "the limits on this" block on
   both directions; the summary is a tap target like everything else. */
.spot-report summary,.species-locations summary{min-height:var(--tap);
  display:flex;align-items:center;font-family:var(--font-mono);
  font-size:11.5px;letter-spacing:.06em;color:var(--link-ink);cursor:pointer}
.spot-report details ul,.species-locations details ul{margin:0 0 0 18px;
  padding:0}
.spot-report details li,.species-locations details li{margin:3px 0;
  max-width:var(--measure);overflow-wrap:anywhere}

/* Guided-setup chip (dashboard + plan entry) ---------------------------- */
/* The "Finish setup (step N of 4)" entry point. A pill, not a card: it has
   to read as a path back into the wizard, not as a verdict or a warning. */
.setup-chip{display:inline-flex;align-items:center;gap:8px;
  min-height:var(--tap);padding:8px 18px;background:var(--sunrise-soft);
  color:var(--fair-ink);border:1px solid var(--sunrise);
  border-radius:var(--radius-pill);font-weight:600;font-size:.9rem;
  text-decoration:none}
.setup-chip:hover{background:var(--sunrise);color:var(--ink);
  text-decoration:none}
