/* DroneWorthy tools — the shared shell, on the v2.1 token system.
 *
 * MIGRATED 2026-07-26 from a local palette to design-system/tokens.css. Every colour below is now
 * a var(--dw-*); there is no hex in this file, which is the token file's own law and the thing
 * scripts/test-design-tokens.mjs now enforces.
 *
 * THE TWO RULINGS THIS FILE OBEYS.
 *
 * 1. ONE ACCENT, AND IT IS AMBER. Brand magenta #C0398F is retired everywhere, including the
 *    public tool front door. The reason is pedagogical rather than aesthetic: brand magenta and
 *    --faa-chart-magenta on the same screen is a teaching hazard, and removing brand magenta
 *    entirely means a learner can never mistake a CTA for Class C.
 *
 * 2. CHART SEMANTICS LIVE ON PAPER, APP CHROME LIVES ON GRAPHITE. The FAA hexes measure 2.25:1
 *    and 3.78:1 on near-black and 7.88:1 and 4.68:1 on --dw-paper. When a colour rule gets broken
 *    "for legibility", check whether the SURFACE is wrong before blaming the colour.
 */

*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
  font:var(--dw-fs-md)/var(--dw-lh-normal) var(--dw-font-body);
  color:var(--dw-ink);
  background:var(--dw-bg);
  display:flex;flex-direction:column;min-height:100dvh;
}
/* One light source and a grain, so the dark surface reads as material rather than as a wireframe.
   Fixed to the viewport on purpose — a gradient per card is what makes dark UIs look cheap. */
body::before{
  content:"";position:fixed;inset:0;pointer-events:none;z-index:0;
  background:var(--dw-light);
}
body::after{
  content:"";position:fixed;inset:0;pointer-events:none;z-index:0;
  background-image:var(--dw-grain);opacity:var(--dw-grain-opacity);
}
/* 🔴 flex-shrink:0 IS LOAD-BEARING, and it is the second half of the height fix below.
 * Once `main` takes an explicit height, the body column is TALLER than 100dvh — which means free
 * space is negative and flexbox starts SHRINKING items to compensate. Everything here defaults to
 * flex-shrink:1, so the browser picked the most compressible sibling and crushed it: measured
 * 2026-08-07, the tool sub-nav went from 49px to 1px — the whole "Live chart / Can I fly? /
 * Aviation weather / Forecast" row silently flattened to a hairline, because `overflow-x:auto`
 * lets it collapse below its content. The bars are chrome; they are never the give in the layout. */
body > *{position:relative;z-index:1;flex-shrink:0}

h1,h2,h3,.h{font-family:var(--dw-font-display);font-weight:var(--dw-fw-medium);line-height:var(--dw-lh-tight)}
a{color:var(--dw-accent)}

header{
  background:var(--dw-glass);backdrop-filter:var(--dw-glass-blur);-webkit-backdrop-filter:var(--dw-glass-blur);
  border-bottom:1px solid var(--dw-line-inset);
  color:var(--dw-ink);padding:var(--dw-space-3) var(--dw-space-4);
  display:flex;gap:var(--dw-space-3);align-items:center;flex-wrap:wrap;
}
header .brand{font-family:var(--dw-font-display);font-weight:var(--dw-fw-bold);text-decoration:none;color:var(--dw-ink)}
header .brand b{color:var(--dw-accent)}
header .tag{font-size:var(--dw-fs-sm);color:var(--dw-dim)}
header nav{margin-left:auto;display:flex;gap:var(--dw-space-4);font-size:var(--dw-fs-sm);flex-wrap:wrap}
header nav a{color:var(--dw-dim);text-decoration:none}
header nav a:hover{color:var(--dw-ink)}
header nav a[aria-current]{color:var(--dw-accent);text-decoration:underline}

.locbar{
  background:var(--dw-surface);border-bottom:1px solid var(--dw-line-inset);
  padding:var(--dw-space-3) var(--dw-space-4);
  display:flex;gap:var(--dw-space-2);flex-wrap:wrap;align-items:center;
}
.locbar form{display:flex;gap:var(--dw-space-2);flex:1 1 320px;min-width:0}
.locbar input{
  flex:1;min-width:0;padding:var(--dw-space-3);
  border:1px solid var(--dw-line);border-radius:var(--dw-radius-sm);
  background:var(--dw-surface-2);color:var(--dw-ink);font:inherit;
}
.locbar input::placeholder{color:var(--dw-dim-2)}

/* Every interactive element clears the touch minimum, on the marketing site as well as the app. */
.btn{
  min-height:var(--dw-touch-min);
  padding:var(--dw-space-3) var(--dw-space-4);
  border:1px solid transparent;border-radius:var(--dw-radius-sm);
  font:inherit;font-weight:var(--dw-fw-medium);cursor:pointer;
  /* THE BUTTON LADDER — LANE DWSURFACES, 2026-08-12.
     Two changes, both small and both deliberate.
     ① The clock. This was --dw-dur-fast (120ms), which is not on the shared scale
        §6.2 asks for; row 344 moved the homepage's .xtile off the identical freehand
        value onto --mo-micro for the same reason. One motion vocabulary across the
        marketing surfaces, not one per file.
     ② The press. Rest → hover → press, and the press RETURNS the lift, so the control
        reads as taking your weight rather than as a colour that changed. The lift is
        1px: this button sits inside a form row next to an input, and a marketing-sized
        3px hop there reads as the row coming apart.
     Only transform/opacity/colour animate — nothing here can move the flex layout that
     the height fix below depends on. */
  transition:transform var(--mo-micro) var(--mo-ease-out),
             background var(--mo-micro) var(--mo-ease-out),
             border-color var(--mo-micro) var(--mo-ease-out),
             box-shadow var(--mo-base) var(--mo-ease-out);
}
.btn:hover{transform:translate3d(0,-1px,0)}
.btn:active{transform:translate3d(0,1px,0)}
.btn-primary{background:var(--dw-accent);color:var(--dw-on-accent);box-shadow:var(--dw-elev-accent)}
.btn-primary:hover{background:var(--dw-accent-quiet)}
.btn-ghost{background:transparent;color:var(--dw-accent);border-color:var(--dw-line)}
.btn-ghost:hover{background:var(--dw-accent-wash)}
/* The gate cannot reach this file's :hover rules — they are not scoped to html.dw-motion,
   because a hover transition is not a thing that hides content and removing it entirely
   would leave the button half-animated. §2⑤: half-animated is worse than static. So the
   reduced-motion contract is met by collapsing the clock (tokens.css sets --mo-* to 1ms
   under the query) and by killing the transform outright here. */
@media (prefers-reduced-motion: reduce){
  .btn{transition:none}
  .btn:hover,.btn:active{transform:none}
}
.btn:focus-visible,input:focus-visible,a:focus-visible,summary:focus-visible{
  outline:none;box-shadow:var(--dw-focus-ring);
}
.hint{font-size:var(--dw-fs-sm);color:var(--dw-dim-2);flex-basis:100%}

/* 🔴 THE FOOTER IS A FLEX CHILD OF THIS BODY, AND WHEN THIS RULE WAS WRITTEN IT DID NOT EXIST.
 *
 * Reported by Robert 2026-08-07: /tools/chart rendered the map as a ~320px strip with the Leaflet
 * attribution clipped and the footer jammed underneath. Nothing was wrong with the map. It was
 * being handed a 320px box.
 *
 * WHY. `scripts/lib-nav.mjs` injects .dw-nav, .dw-subnav and .dw-footer at build time — a change
 * made 2026-08-01, four days after this file's layout was written. The footer is three columns of
 * links plus three paragraphs of legal text, call it 380px, and it landed as a sibling of `main`
 * inside `body{min-height:100dvh;display:flex;flex-direction:column}`. `main{flex:1}` grows by
 * DISTRIBUTING FREE SPACE, and once a 380px sibling arrived there was none left — free space went
 * negative — so main collapsed to its content minimum, which is exactly `#map{min-height:320px}`.
 *
 * ⭐ THE LESSON, because it is the third time this shape has bitten this repo: injecting shared
 * chrome changes the LAYOUT CONTRACT of every page it lands on. The injection was tested for
 * presence — is the nav there, is the footer there — and presence is not fit. A page can have all
 * the right elements and still be broken by them.
 *
 * `flex:1` cannot be repaired here; there is nothing to distribute. So the app region takes an
 * explicit height and the footer sits below the fold, which is where a footer belongs. The literal
 * 232px is the measured desktop chrome (nav 60 + subnav 68 + locbar 104) and is only the FALLBACK:
 * tools.js measures the real thing into --dw-shell-top on load and on resize, because that stack
 * wraps at narrow widths and a number in a stylesheet cannot know that. */
main{
  flex:0 0 auto;display:grid;grid-template-columns:1fr 420px;
  height:max(520px, calc(100dvh - var(--dw-shell-top, 232px)));
}
/* The map is the chart. It sits on paper, and the boundary to graphite is drawn deliberately. */
#map{background:var(--dw-paper);min-height:320px;border-right:var(--dw-plate-edge-w) solid var(--dw-plate-edge)}
.panel{background:var(--dw-surface);border-left:1px solid var(--dw-line-inset);overflow:auto;padding:var(--dw-space-4)}
/* 🔴 dvh, NEVER vh, ON ANYTHING THAT HAS TO FIT A PHONE SCREEN.
 *
 * On iOS `vh` resolves against the LARGE viewport — the height the page would have if the browser
 * chrome were retracted — while `dvh` resolves against what is actually visible right now. On an
 * iPhone they differ by roughly 90px. Every CONTAINER in this file was already `dvh` (body above,
 * `body.dw-fs main` below) and only these two ROW definitions were `vh`, so the rows were sized
 * against a taller viewport than the box holding them and the overflow went off the bottom edge.
 *
 * That is not cosmetic. The panel is where the ANSWER lives — "Class G · uncontrolled", the floor,
 * whether Part 107 needs authorization. Reported 2026-07-26 on an iPhone: in full screen the map
 * filled the screen and the verdict was cut off behind the browser bar. A tool that shows you a
 * chart but hides the decode is the FAA's problem restated, which is the exact thing this product
 * exists to not be.
 */
@media (max-width:860px){
  /* Was 46vh, which read as a cramped strip once the locbar's three-line hint had taken its share.
     58dvh against the real visible height gives the chart room to be readable, and the panel flows
     underneath — this mode is allowed to scroll.
     `height:auto` UNDOES the desktop rule above on purpose: here the rows govern and the panel is
     allowed to run as long as its content, because this mode scrolls. Leaving the desktop height in
     place would cap the panel and re-hide the verdict — the exact bug the block below is about. */
  main{grid-template-columns:1fr;grid-template-rows:58dvh auto;height:auto;min-height:0}
  /* The 320px floor is a desktop-side guarantee. On a phone in landscape it is TALLER than the
     track it sits in, which makes the map push the panel off-screen — reintroducing the same bug
     from the other direction. The track sizes govern here. */
  #map{min-height:0;border-right:0;border-bottom:var(--dw-plate-edge-w) solid var(--dw-plate-edge)}
  .panel{border-left:0;border-top:1px solid var(--dw-line-inset)}
}

.card{
  border:1px solid var(--dw-line-inset);border-radius:var(--dw-radius-md);
  padding:var(--dw-space-4);margin-bottom:var(--dw-space-3);
  background:var(--dw-surface-2);box-shadow:var(--dw-elev-1);
}
/* A card carrying chart semantics flips to paper — never the other way round. */
.card.answer,.card.teach{background:var(--dw-paper);color:var(--dw-paper-ink);box-shadow:var(--dw-elev-2)}
.card.answer .k,.card.teach .k{color:var(--dw-paper-dim)}
.card.answer a,.card.teach a{color:var(--dw-accent-quiet)}
.card .h{margin-bottom:var(--dw-space-2)}

.verdict{font-family:var(--dw-font-display);font-weight:var(--dw-fw-bold);font-size:var(--dw-fs-lg);margin-bottom:var(--dw-space-2)}
.verdict.meets{color:var(--dw-good)} .verdict.below{color:var(--dw-bad)} .verdict.unknown{color:var(--dw-caution)}
.verdict-sm{font-family:var(--dw-font-display);font-weight:var(--dw-fw-bold);font-size:var(--dw-fs-sm);margin-bottom:var(--dw-space-1)}
.verdict-sm.meets{color:var(--dw-good)} .verdict-sm.below{color:var(--dw-bad)} .verdict-sm.unknown{color:var(--dw-caution)}

.pill{display:inline-block;padding:2px var(--dw-space-2);border-radius:var(--dw-radius-pill);font-size:var(--dw-fs-xs);font-weight:var(--dw-fw-bold)}
.pill-need{background:var(--dw-state-miss-fill);color:var(--dw-bad)}
.pill-ok{background:var(--dw-state-mastered-fill);color:var(--dw-good)}
.pill-warn{background:var(--dw-accent-wash);color:var(--dw-accent)}

.k{color:var(--dw-dim);font-size:var(--dw-fs-sm)}
.v{font-weight:var(--dw-fw-medium);text-align:right}
.row{display:flex;justify-content:space-between;gap:var(--dw-space-3);padding:var(--dw-space-1) 0;border-bottom:1px dashed var(--dw-line-inset)}
.row:last-child{border-bottom:0}

.caveat{
  background:var(--dw-accent-wash);border-left:4px solid var(--dw-accent);
  padding:var(--dw-space-3);border-radius:0 var(--dw-radius-sm) var(--dw-radius-sm) 0;
  font-size:var(--dw-fs-sm);margin-top:var(--dw-space-3);
}
.src{font:var(--dw-fs-xs)/var(--dw-lh-snug) var(--dw-font-mono);color:var(--dw-dim-2);word-break:break-word}
.disclaim{font-size:var(--dw-fs-sm);color:var(--dw-dim-2);border-top:1px solid var(--dw-line-inset);margin-top:var(--dw-space-2);padding-top:var(--dw-space-3)}
.muted{color:var(--dw-dim-2)} .err{color:var(--dw-bad)}
.prompt{color:var(--dw-dim);text-align:center;padding:var(--dw-space-5) var(--dw-space-2)}
details{margin-top:var(--dw-space-3)} summary{cursor:pointer;font-weight:var(--dw-fw-medium);font-size:var(--dw-fs-sm)}
.layers{display:flex;gap:var(--dw-space-4);flex-wrap:wrap;font-size:var(--dw-fs-sm);margin-bottom:var(--dw-space-3)}
.layers label{display:flex;gap:var(--dw-space-2);align-items:center;cursor:pointer;min-height:var(--dw-touch-min)}

/* Fields the decoder did not recognise. Rendered, never hidden: while the upstream shapes are
   unconfirmed this list is the only thing standing between a wrong guess and a confident lie.
   It uses the PLACEHOLDER tokens, whose whole job is to say "this is an empty slot, not content". */
.unmapped{margin-top:var(--dw-space-3);font-size:var(--dw-fs-xs);color:var(--dw-dim-2);
  border-top:1px dashed var(--dw-placeholder-line);padding-top:var(--dw-space-2)}
.unmapped table{width:100%;border-collapse:collapse}
.unmapped td{padding:3px 4px;vertical-align:top;border-bottom:1px solid var(--dw-line-inset)}
.unmapped td.key{font-family:var(--dw-font-mono);white-space:nowrap;padding-right:var(--dw-space-3);color:var(--dw-placeholder-ink)}

/* ── full screen ────────────────────────────────────────────────────────── */
/* ⚠️ .dw-nav AND .dw-subnav ARE ON THIS LIST BECAUSE THEY ARE <nav>, NOT <header>.
   Same 2026-08-01 chrome injection as the height bug above. `footer` caught the injected footer by
   luck — it really is a <footer> element — but the two injected bars are <nav>, so full screen was
   hiding the page's own header (already stripped at build time, so hiding nothing) and leaving
   ~128px of sticky bars sitting on top of a map that had just been told to be exactly 100dvh tall.
   Full screen cannot scroll, so that came straight off the bottom. */
body.dw-fs header,
body.dw-fs .dw-nav,
body.dw-fs .dw-subnav,
body.dw-fs .locbar,
body.dw-fs footer { display:none }
body.dw-fs main { height:100dvh }
body.dw-fs .panel { max-height:100dvh }
/* ⭐ FULL SCREEN CANNOT SCROLL, so the rows must ADD UP TO EXACTLY the container.
 *
 * `main` is height:100dvh here. The old `62vh auto` broke that twice over: 62vh overshoots 62dvh by
 * ~55px on an iPhone, and `auto` then let the panel size to its CONTENT — so map + panel exceeded
 * 100dvh and the answer went off the bottom with no way to scroll to it, because full screen hides
 * the page chrome and the overflow had nowhere to go.
 *
 * `1fr 38dvh` is exact by construction: the panel is a fixed 38dvh and already carries
 * `overflow:auto`, so long results scroll INSIDE it, and the map takes whatever is left. The verdict
 * is now always on screen at any phone height, in either orientation. */
@media (max-width:860px){ body.dw-fs main { grid-template-rows:1fr 38dvh } }
.dw-fs-ctl a{
  display:block;width:30px;height:30px;line-height:30px;text-align:center;
  font-size:var(--dw-fs-md);color:var(--dw-paper-ink);background:var(--dw-paper);text-decoration:none;
}
.dw-fs-ctl a:hover{ background:var(--dw-paper-2) }

footer{
  background:var(--dw-surface);color:var(--dw-dim-2);font-size:var(--dw-fs-xs);
  padding:var(--dw-space-3) var(--dw-space-4);text-align:center;
  border-top:1px solid var(--dw-line-inset);
}
footer a{color:var(--dw-dim)}
.skip{position:absolute;left:-999px}
.skip:focus{left:var(--dw-space-2);top:var(--dw-space-2);background:var(--dw-surface);color:var(--dw-ink);
  padding:var(--dw-space-2);z-index:9999;border-radius:var(--dw-radius-xs);box-shadow:var(--dw-focus-ring)}
