/* ---------- the faces ----------
   IBM Plex, served from this origin. `font-src 'self'` in headers.py would
   block a Google Fonts link outright, and self-hosting is the better answer
   regardless: no third-party request before first paint, and nothing about a
   font CDN to explain on the privacy page.

   The sans ships as one variable file per range, 100 to 700 - smaller than
   the three static weights this sheet asks for, and it covers the 700 that a
   bare <b> inherits. Plex Mono has no variable cut, so it ships as the two
   weights the figures actually use. Both are subset the way Google serves
   them: a browser fetches latin-ext only when a glyph needs it, which is what
   keeps a page of Berlin fares from paying for Lodz.

   `?v=1` is what makes StaticCache in headers.py answer with the immutable
   year rather than the unversioned hour. The filenames never change, so
   neither does the number.

   IBM Plex is under the OFL - see fonts/OFL.txt. */
@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 100 700;
  font-display: swap;
  src: url("fonts/plex-sans-var-latin.woff2?v=1") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 100 700;
  font-display: swap;
  src: url("fonts/plex-sans-var-latin-ext.woff2?v=1") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/plex-mono-400-latin.woff2?v=1") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/plex-mono-400-latin-ext.woff2?v=1") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("fonts/plex-mono-600-latin.woff2?v=1") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("fonts/plex-mono-600-latin-ext.woff2?v=1") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  /* Green light. Two colours with one job each: navy is the app - buttons,
     links, the mark, the focus ring - and green is a fare under your limit,
     and nothing else. The pink this replaced did both, and also the notices,
     so the page was pink before it had said anything and the cheapest day
     was the same colour as the button beside it.

     The ground and the neutrals are a cool grey with no hue of their own, so
     the green is the only colour on a page of fares that means something. */
  --bg: #f3f5f7;
  --panel: #ffffff;
  --panel-soft: #f7f9fa;
  /* Inside a text field: the panel's white by day, a step under it at night. */
  --field: #ffffff;
  --ink: #131a22;
  --ink-soft: #3c4754;
  /* 5.2:1 on the page and 5.7:1 on the panel, and this is the colour of every
     eyebrow, meta row and timestamp in the app - 35 rules here and 78 uses in
     the templates, most of them at 11-13px. Dark enough to clear AA on white,
     on --bg, on --panel-soft and on the palest cells. */
  --muted: #5d6875;
  --line: #dce2e7;
  --line-soft: #e8ecef;
  /* A border under the cursor, one step past --line. */
  --line-strong: #c7cfd6;
  --accent: #1d3f72;
  /* The same colour as a triple, because `rgba()` cannot take a hex
     variable and the five washes and glows that need one were the only
     places in this file still holding the old accent by hand. */
  --accent-rgb: 29, 63, 114;
  --accent-hover: #142f58;
  --accent-soft: #e5ecf6;
  /* Whatever is written on the accent. White by day; at night the accent is
     lifted too far for white to clear AA on it, so it takes the ink instead. */
  --on-accent: #ffffff;
  --warn: #8a5a1f;
  --danger: #a32f3f;

  /* The verdict on a fare, wherever one is judged against the number you set:
     the cheapest band of the heat scale, so a fare does not change hue on the
     way from the dashboard to its own detail page. Its own tints, not the
     accent's - a fare under the limit and a selected date are different
     things, and they used to be drawn in the same pink.

     `--miss` is the other half, and it is deliberately not the danger red it
     used to borrow: a fare over your limit is an ordinary answer, not an
     error, and colouring it like a failed form made every list of options
     look like a list of problems. */
  --hit: var(--heat-1);
  --hit-soft: #e4f3e9;
  --hit-line: #bfe3cc;
  --miss: #53616f;
  --miss-soft: #eef1f4;
  --miss-line: #d8dee4;

  /* The heat scale. One ramp, used by the calendar cells, the destination
     plates and the alert chips alike, so a fare is the same colour wherever
     it is shown. Inside the limit is one green in three steps that get
     darker as the fare drops, so the cheapest day is the darkest cell and
     the order reads with the colour taken out. Outside it is plain grey.
     The step from band 3 to band 4 is the step from yes to no, which is why
     there is no smooth blend across it.

     It used to be pink, red and orange. Pink and red are almost equally dark
     (L* 43 and 48), so only their hue told them apart, and nobody could say
     without the key whether pink or orange was the cheaper one. Each band
     carries the ink that clears AA on it, so nothing has to guess at the
     pairing. */
  --heat-1: #0a6b3c; --heat-1-ink: #ffffff;   /* half the limit or under */
  --heat-2: #3aa865; --heat-2-ink: #04200e;
  --heat-3: #b6e3c4; --heat-3-ink: #0e3a21;   /* up to the limit exactly */
  --heat-4: #d5dbe1; --heat-4-ink: #2d3a47;   /* over it */
  --heat-5: #eceff2; --heat-5-ink: #5a6572;
  /* The dashboard's opener: a wash of the green, so the sentence about what
     is under your limit sits in the colour it is about. */
  --blush: #e6f2ea;
  --blush-ink: #36503f;

  /* Two faces, and the rule for which is which: Plex Sans says everything the
     page says, Plex Mono says everything the page counted. The fallbacks are
     the stack the site used to be set in, so a page drawn before the woff2
     lands is the old page rather than Times. */
  --font-ui: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
             Roboto, Helvetica, Arial, sans-serif;
  --font-num: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas,
              monospace;

  /* Type. Ten named steps on a major third, replacing the twenty-five sizes
     that were here - six of them fractional, which is what a number nudged
     until one particular line looked right leaves behind. Ten rather than the
     six a page of prose would need: this app sets a 48px price figure at one
     end and a seven-column date grid at the other, and pretending otherwise
     just puts the odd sizes back as one-offs. */
  --t-display: 48px;   /* the headline fare, and nothing else */
  --t-xl: 38px;        /* the front page headline */
  --t-lg: 30px;        /* secondary page headlines, the figures in the steps */
  --t-h1: 24px;        /* h1, and a price on a card */
  --t-h2: 20px;        /* h2 and section headings */
  --t-lead: 18px;      /* a lede under a headline */
  --t-body: 16px;
  --t-small: 14px;
  --t-micro: 12px;     /* labels, meta, timestamps */
  --t-grid: 10px;      /* the date grid only - a month in 40px cells */

  /* Space. Every margin comes off this, so a component owns its own spacing
     and no use site has to correct it. Roughly a hundred of the inline
     style="margin:0" attributes in the templates were that correction. */
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 24px;
  --s-6: 32px;
  --s-7: 48px;

  /* Corners. Three, plus the pill - down from fifteen. */
  --r-lg: 12px;        /* panels and cards */
  --r-md: 8px;         /* fields, buttons, notices */
  --r-sm: 4px;         /* small cells and marks */
  --r-pill: 999px;     /* pills, and any rail that should have round ends */

  --maxw: 940px;
  --measure: 62ch;     /* prose, wherever it is not in a narrow column already */
  --shadow-sm: 0 1px 2px rgba(30, 34, 38, .05);
  --shadow-md: 0 6px 24px rgba(30, 34, 38, .07);
}

/* ---------- at night ----------
   The dark palette, and the few rules that hold a colour of their own, are in
   dark.css: one copy that both the system setting and the switch in the
   header turn on. See the top of that file. */

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: var(--t-body)/1.6 var(--font-ui);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

.wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 24px; }

/* ---------- header ---------- */
header.top {
  border-bottom: 1px solid var(--line);
  background: rgba(255, 255, 255, .82);
  backdrop-filter: saturate(140%) blur(8px);
  position: sticky; top: 0; z-index: 20;
}
header.top .wrap {
  display: flex; align-items: center; gap: 28px; height: 62px;
}
.brand {
  font-weight: 600; letter-spacing: -0.015em; color: var(--ink);
  display: inline-flex; align-items: center; gap: 9px;
}
.brand:hover { text-decoration: none; }
.brand .mark {
  width: 22px; height: 22px; border-radius: var(--r-sm); background: var(--accent);
  color: var(--on-accent); display: grid; place-items: center; font-size: var(--t-micro);
}
nav.main { display: flex; gap: 20px; margin-left: auto; align-items: center; }
nav.main a { color: var(--muted); font-size: var(--t-small); position: relative; }
nav.main a:hover { color: var(--ink); text-decoration: none; }
/* Where you are, said once and said persistently. Hover used to draw exactly
   this - both were `color: var(--ink)` - so the page you were on and the link
   under the cursor were the same picture, and the row said nothing at all
   about where in the app you had got to. */
nav.main a.active { color: var(--ink); font-weight: 600; }
nav.main a.active::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -7px; height: 2px;
  border-radius: var(--r-sm); background: var(--accent);
}
/* The account action, which is not a section of the app and should not read
   as one: a guest signing in, or a stranger making an account. Never "Sign
   out" - the way out needs no encouragement. */
nav.main a.nav-act {
  padding: 4px 13px; border: 1px solid var(--line); border-radius: var(--r-pill);
  background: var(--panel); color: var(--ink-soft);
  transition: border-color .12s, background-color .12s;
}
nav.main a.nav-act:hover { background: var(--panel-soft); border-color: var(--line-strong); color: var(--ink); }

/* Light or dark, whatever the system says. Round and drawn like the account
   pill beside it, because it is the same kind of thing - an action on the
   page, not a place in the app. app.js shows it; without the script it could
   do nothing, so it stays hidden and the page just follows the system. The
   icon is switched by dark.css: a moon by day, a sun at night. */
.theme-toggle {
  display: grid; place-items: center; flex: none;
  width: 32px; height: 32px; padding: 0; margin-left: -8px;
  border: 1px solid var(--line); border-radius: var(--r-pill);
  background: var(--panel); color: var(--ink-soft); cursor: pointer;
  transition: border-color .12s, background-color .12s;
}
.theme-toggle:hover { background: var(--panel-soft); border-color: var(--line-strong); color: var(--ink); }
.theme-toggle[hidden] { display: none; }
.theme-toggle svg { width: 16px; height: 16px; }
.theme-toggle .i-sun { display: none; }

/* Unread alerts. A pill, like every other small round label in the app - it
   was the one chip set to the field radius, which put two different corner
   radii side by side in the same six inches of header. */
.badge {
  display: inline-block; min-width: 18px; padding: 1px 6px; margin-left: 5px;
  background: var(--accent); color: var(--on-accent); border-radius: var(--r-pill);
  font-size: var(--t-micro); line-height: 16px; text-align: center;
  font-variant-numeric: tabular-nums;
}

main { padding: 40px 0 80px; }

/* ---------- typography ---------- */
h1 { font-size: var(--t-h1); font-weight: 600; letter-spacing: -0.02em; margin: 0 0 6px; }
/* At 16px this was one pixel larger than body text, which is why every page
   needed a bordered panel to show where a section started. */
h2 {
  font-size: var(--t-h2); font-weight: 600; letter-spacing: -0.015em;
  margin: var(--s-6) 0 var(--s-3);
}
/* A heading that opens a panel is not starting a new section inside it. */
.panel > h2:first-child, .head-row + h2, h2:first-child { margin-top: 0; }
.lede { color: var(--muted); margin: 0 0 var(--s-5); max-width: var(--measure); }
/* A lede that sits beside a button in a header row is not introducing the
   page, it is finishing the title. */
.head-row .lede { margin: 0; }
.eyebrow {
  font-size: var(--t-micro); letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--muted); margin: 0 0 var(--s-2);
}
/* An eyebrow that opens a fieldset is a heading for the fields under it, not
   another label beside them: it carries the ink and the weight, and it gets
   the step a heading gets rather than the 8px a run of labels would share. */
fieldset > .eyebrow:first-child {
  color: var(--ink); font-weight: 600; margin-bottom: var(--s-4);
}
.muted { color: var(--muted); }
/* A price that has gone stale, or a check that is failing. Not an error state
   with a red box - the page still works, it is just older than it should be. */
.warn { color: var(--warn); }
/* Zero, deliberately. These sit in stacks under a price or a heading where
   the browser's 1em top and bottom is always wrong - which is why sixty-odd
   of them in the templates carried an inline margin to cancel it. Where two
   run together the gap belongs to the pair, not to either one. */
.small { font-size: var(--t-small); margin: 0; }
.small + .small { margin-top: var(--s-1); }
.num-face { font-variant-numeric: tabular-nums; }

/* ---------- the figures ----------
   Plex Mono was drawn alongside Plex Sans, so a fare set in it still belongs
   to the page rather than looking pasted in from a terminal - and every digit
   is the same width, which is what stops a column of prices shifting sideways
   each time a recheck lands.

   Money, and the counts that are read against each other. Never a sentence,
   which is what keeps three near-misses out of the list: `.gauge-now` and
   `.gauge-limit` read "EUR 49 cheapest now", `td.num` carries "3 hours ago"
   and a booking link as often as a figure, and `.flow-fig` holds its own
   caption - so that one takes the mono and hands the sans back to its
   `<small>` below. */
.num-face,
.price-big,
.headline .figure,
.flow-fig,
.cal-day i,
.opt-price,
.dest-price,
.pick-fare,
.plate,
td.col-price {
  font-family: var(--font-num);
}
.flow-fig small { font-family: var(--font-ui); }

/* A line of explanation at the top of a panel, before the thing it explains.
   Three templates carried margin:-2px 0 16px for this. */
.panel > .small:first-child { margin: 0 0 var(--s-4); }

/* A note that belongs to the heading above it rather than to what follows.
   Four auth pages each carried margin:-10px 0 20px to pull one back up under
   an h1 whose bottom margin is set for a form, not for a sentence. Still a
   negative, but computed off the scale and stated once. */
.subhead { margin: calc(var(--s-2) - var(--s-5)) 0 var(--s-5); }

/* Spacing that genuinely belongs to a place rather than to a component. Nine
   steps off the scale - greppable, and never a number typed into a template. */
.mt-0 { margin-top: 0; }
.mt-1 { margin-top: var(--s-1); }
.mt-2 { margin-top: var(--s-2); }
.mt-3 { margin-top: var(--s-3); }
.mt-4 { margin-top: var(--s-4); }
.mb-0 { margin-bottom: 0; }
.mb-2 { margin-bottom: var(--s-2); }
.mb-4 { margin-bottom: var(--s-4); }
.mt-5 { margin-top: var(--s-5); }
.mt-6 { margin-top: var(--s-6); }
.mb-5 { margin-bottom: var(--s-5); }
.ml-2 { margin-left: var(--s-2); }
.m-0 { margin: 0; }
.measure { max-width: var(--measure); }
.center { text-align: center; }
.right { text-align: right; }
/* Narrower than the page, wider than the sign-in card: a page that is one
   column of prose and one button. */
.narrow { max-width: 34rem; }
.bullets { padding-left: var(--s-5); }
/* A fieldset that opens a form has nothing above it to be ruled off from. */
fieldset.flush { border-top: none; padding-top: 0; }
/* A second line inside a cell or a row. */
.line { display: block; }

/* ---------- panels ---------- */
.panel {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-lg); padding: 22px 24px; margin-bottom: 18px;
  box-shadow: var(--shadow-sm);
}
.panel.tight { padding: 16px 18px; }

.head-row {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 20px; margin-bottom: 26px; flex-wrap: wrap;
}

/* ---------- watch cards ---------- */
.card-link { display: block; color: inherit; }
.card-link:hover { text-decoration: none; }
.card-link:hover .panel { border-color: var(--line-strong); box-shadow: var(--shadow-md); }
.card-link .panel { transition: border-color .15s, box-shadow .15s; }

.route {
  font-size: var(--t-h2); font-weight: 600; letter-spacing: -0.015em;
  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
  margin: 0 0 var(--s-2);
}
.route .arrow { color: var(--muted); font-weight: 400; }
.route .sub { font-size: var(--t-small); font-weight: 400; color: var(--muted); }
.price-big {
  font-size: var(--t-h1); font-weight: 600; letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums; margin: 0;
}
.price-big + .small { margin-top: var(--s-1); }
.price-big.hit { color: var(--hit); }

.meta { display: flex; flex-wrap: wrap; gap: 6px 18px; color: var(--muted); font-size: var(--t-small); }

.bar {
  height: 3px; background: var(--line-soft); border-radius: var(--r-pill);
  overflow: hidden; margin-top: var(--s-4);
}
.bar > span { display: block; height: 100%; background: var(--accent); }

/* price against target */
.track { margin-top: 16px; }
.track .rail {
  position: relative; height: 6px; border-radius: var(--r-pill);
  background: linear-gradient(90deg, #dde3e9, #eaeef1);
}
.track .fill {
  position: absolute; inset: 0 auto 0 0; border-radius: var(--r-pill);
  background: var(--accent);
}
.track .goal {
  position: absolute; top: -4px; width: 2px; height: 14px;
  background: var(--ink); opacity: .35; border-radius: var(--r-sm);
}
.track .legend {
  display: flex; justify-content: space-between; gap: 12px;
  font-size: var(--t-micro); color: var(--muted); margin-top: 7px;
}

/* ---------- the headline price ----------
   One number is large on this page and it is the fare. The limit used to be
   set beside it at the same size under an eyebrow of its own, which made a
   setting you typed look like a result we found, and gave the block two
   headlines competing for the same glance. It is said once now, in the
   sentence under the price, and drawn once as the mark on the gauge. */
.headline { padding: 26px 24px 24px; }
.headline .figure {
  font-size: var(--t-display); line-height: 1.05; font-weight: 600; letter-spacing: -0.015em;
  font-variant-numeric: tabular-nums; margin: 0;
}
.headline .figure.hit { color: var(--hit); }
/* The state as a sentence rather than a label above the number. It carries the
   limit, so nothing below it has to say the limit again. */
.headline .figure-say {
  font-size: var(--t-body); color: var(--ink-soft); margin: 6px 0 0; max-width: 52ch;
}

/* The trip, and the way off this page. Both were set in the same 13px grey as
   the caveats under them, so the one thing here worth acting on read as their
   caption. */
.headline .trip {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px 20px; flex-wrap: wrap; margin-top: 18px;
}
.headline .trip-when {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 16px;
  font-size: var(--t-body); margin: 0;
}
.headline .trip-when b { font-weight: 600; }
.btn.compact { padding: 6px 14px; font-size: var(--t-small); }

/* ---------- the gauge ----------
   Both numbers on one scale, so the distance between them is the picture. The
   bar this replaces filled to 100% and turned green the moment the limit was
   beaten, which drew exactly the same thing for EUR51 against EUR100 as for
   EUR99 against it.

   The rail runs from zero to a tenth past whichever number is larger, so the
   larger one always sits at 91% and the smaller one lands where it belongs.
   That is also why the labels below need no positioning: the left-hand label
   is always the smaller value. */
.gauge { margin-top: 20px; }
.gauge-rail {
  position: relative; height: 10px; border-radius: var(--r-pill);
  background: var(--line-soft);
}
/* Ink while the fare is still over the limit, and the hit colour once it is
   under. A fare over the limit fills most of the rail - that is the point of
   the scale - and filled in the accent it read as an achievement rather than
   an overshoot. The crossing is the only thing this colour marks here. */
.gauge-fill {
  position: absolute; inset: 0 auto 0 0; border-radius: var(--r-pill);
  background: var(--ink-soft);
}
.gauge.hit .gauge-fill { background: var(--hit); }
/* Taller than the rail, so it reads as a mark on a scale rather than a join.
   Which side of the fill it lands on is fixed by which number is larger: under
   the limit the mark sits out on the pale rail and is drawn in ink; over it,
   the fill has already run past and the mark is cut out of it instead. */
.gauge-mark {
  position: absolute; top: -5px; width: 2px; height: 20px; margin-left: -1px;
  background: var(--ink); border-radius: var(--r-pill);
}
.gauge:not(.hit) .gauge-mark { background: var(--panel); }
.gauge-labels {
  display: flex; justify-content: space-between; gap: 12px;
  margin-top: 12px; font-size: var(--t-small); color: var(--muted);
}
.gauge-now { color: var(--ink-soft); font-weight: 600; font-variant-numeric: tabular-nums; }
.gauge.hit .gauge-now { color: var(--hit); }
.gauge-limit { font-variant-numeric: tabular-nums; }

/* What the number is, how old it is, and whether it is any good: three facts,
   one to a line. They were four sentences of 13px grey stacked at 4px apart,
   which is a paragraph, not a list, and read as one grey block nobody
   finished. */
.headline .facts {
  list-style: none; margin: 20px 0 0; padding: 14px 0 0;
  border-top: 1px solid var(--line-soft);
  font-size: var(--t-small); color: var(--muted);
}
.headline .facts li + li { margin-top: 5px; }

/* A sentence that stands between panels rather than inside one. */
.aside { margin: 0 0 18px; }

@media (max-width: 520px) {
  .headline .figure { font-size: var(--t-xl); }
  .headline .trip-when { font-size: var(--t-body); }
}

/* the three cheapest dates
   A row that beats the limit keeps the green the rest of the app uses for a
   hit; one that does not is tinted with --miss and says how far over it is.
   Colour is never the only signal - the note next to it spells out the gap. */
.opts { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--line-soft); }
.opt {
  display: grid;
  /* Fixed columns, not auto: each row is its own grid, so only fixed widths
     line the notes and links up with each other down the list. */
  grid-template-columns: 92px minmax(0, 1fr) 96px 92px;
  gap: 4px 14px; align-items: baseline;
  padding: 9px 12px; font-size: var(--t-small);
  border: 1px solid transparent; border-left-width: 3px;
  border-radius: var(--r-md);
}
.opt + .opt { margin-top: 6px; }
.opt-price { font-weight: 600; font-variant-numeric: tabular-nums; }
.opt-dates .muted { white-space: nowrap; }
.opt-note, .opt-link { font-size: var(--t-micro); white-space: nowrap; text-align: right; }
.opt.hit { background: var(--hit-soft); border-color: var(--hit-line); border-left-color: var(--hit); }
.opt.hit .opt-price, .opt.hit .opt-note { color: var(--hit); }
.opt.over { background: var(--miss-soft); border-color: var(--miss-line); border-left-color: var(--miss); }
.opt.over .opt-price, .opt.over .opt-note { color: var(--miss); }
/* The way out of the row. It was the airline's name in 12.5px grey, right
   aligned, which is how a *label* is set - so the one clickable thing on the
   row read as the caption for it. */
.opt-link a.book {
  display: inline-block; font-weight: 600; text-decoration: none;
  padding: 3px 9px; border-radius: var(--r-pill); white-space: nowrap;
  border: 1px solid currentColor;
}
.opt-link a.book:hover { background: var(--panel); text-decoration: none; }
.opt.hit .opt-link a.book { color: var(--hit); }
.opt.over .opt-link a.book { color: var(--miss); }

/* One card per trip, with the trip's other prices under it. `.fare` wraps the
   pair so a filter can hide a trip and its detail with one attribute. */
.opt-toggle {
  grid-column: 2 / -1; justify-self: start;
  background: none; border: 0; padding: 2px 0; margin-top: 2px;
  /* --ink-soft rather than --muted: these are controls, and --muted at
     12.5px is the app's quiet-caption grey, which does not clear AA for
     something a person is meant to click. */
  font-size: var(--t-micro); color: var(--ink-soft); text-decoration: underline;
  text-underline-offset: 2px; cursor: pointer; box-shadow: none;
}
.opt-toggle:hover { background: none; color: var(--ink); }
.opt-toggle[aria-expanded="true"]::after { content: " ▴"; }
.opt-toggle[aria-expanded="false"]::after { content: " ▾"; }
.opt-detail {
  margin: 2px 0 0 14px; padding: 6px 0 6px 12px;
  border-left: 2px solid var(--line-soft);
}
.opt-row {
  display: grid;
  grid-template-columns: 78px minmax(0, 1fr) 96px 92px;
  gap: 2px 14px; align-items: baseline;
  padding: 5px 12px; font-size: var(--t-small); border-radius: var(--r-md);
}
.opt-row + .opt-row { margin-top: 2px; }
.opt-row .opt-price { font-weight: 600; }
.opt-row.hit .opt-price, .opt-row.hit .opt-note { color: var(--hit); }
.opt-row.over .opt-price, .opt-row.over .opt-note { color: var(--miss); }

/* Sort and filter. Hidden in the markup and unhidden by app.js, so a page
   without JavaScript shows the list it always showed rather than a row of
   controls that do nothing.

   One raised bar rather than a rule under a paragraph: it is the handle on the
   list below it, and the paragraph it used to sit under - four lines naming
   the airlines the rows already name - has gone. There is no month select. The
   window is something the traveller typed, so a control offering to hide half
   of it was undoing a decision they had just made. */
.listbar {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2) 10px;
  padding: 7px 8px 7px 14px; margin: 0 0 var(--s-3);
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-pill);
  box-shadow: var(--shadow-sm), 0 10px 24px -18px rgba(30, 34, 38, .4);
}
.sortby { display: flex; align-items: center; gap: 2px; }
.sortby button {
  background: none; border: 0; color: var(--ink-soft); font-weight: 500;
  padding: 6px 14px; border-radius: var(--r-pill); font-size: var(--t-micro);
  cursor: pointer; box-shadow: none;
}
.sortby button:hover { background: var(--panel-soft); color: var(--ink); }
.sortby button.on {
  background: var(--accent); color: var(--on-accent); font-weight: 600;
}
.sortby button.on:hover { background: var(--accent-hover); }

/* A select that reads as a chip rather than a form field: this is a list
   control, and it sits beside a list, not on a page of questions. */
.drop {
  display: inline-flex; align-items: center; gap: 5px; margin: 0;
  font-size: var(--t-micro); color: var(--muted);
  background: var(--panel-soft); border: 1px solid var(--line);
  border-radius: var(--r-pill); padding: 4px 6px 4px 12px;
}
.drop select {
  font-size: var(--t-micro); font-weight: 600; color: var(--ink);
  border: 0; background: none; padding: 1px 2px; width: auto; margin: 0;
  cursor: pointer;
}
.switch {
  display: inline-flex; align-items: center; gap: var(--s-2); margin: 0;
  font-size: var(--t-micro); color: var(--ink-soft); cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--r-pill);
  padding: 5px 13px 5px 10px;
}
.switch:has(input:checked) {
  background: var(--hit-soft); border-color: var(--hit-line); color: var(--hit);
}
.switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.switch i {
  flex: none; width: 30px; height: 17px; border-radius: var(--r-pill);
  background: var(--line-strong); position: relative; transition: background .16s ease;
}
.switch i::after {
  content: ""; position: absolute; top: 2px; left: 2px; width: 13px;
  height: 13px; border-radius: 50%; background: #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .25); transition: transform .16s ease;
}
.switch input:checked + i { background: var(--hit); }
.switch input:checked + i::after { transform: translateX(13px); }
.switch input:focus-visible + i { outline: 2px solid var(--accent); outline-offset: 2px; }
.listbar-count { margin: 0 6px 0 auto; }

/* ---------- one card per destination ----------
   The price is a plate in the colour the calendar and the dashboard already
   paint it: green inside the limit, grey outside it, so a fare is one colour
   wherever it is shown. The card is three columns and they are fixed, which is
   the whole point - the way out of the row used to be a pill in a 92px column
   that "Book on Kiwi.com" did not fit in, and it hung off the end of the card.
   The airline is named above the button instead, so the button is one word and
   cannot outgrow its column. */
.dests { display: flex; flex-direction: column; gap: var(--s-2); }
.dest {
  display: grid; grid-template-columns: 92px minmax(0, 1fr) 132px;
  background: var(--panel); border-radius: var(--r-lg); overflow: hidden;
  box-shadow: 0 1px 2px rgba(30, 34, 38, .06),
              0 12px 28px -22px rgba(19, 26, 34, .55);
  transition: box-shadow .18s ease, transform .18s ease;
}
.dest:hover {
  transform: translateY(-1px);
  box-shadow: 0 2px 4px rgba(30, 34, 38, .07),
              0 18px 34px -22px rgba(19, 26, 34, .6);
}
.dest-plate {
  display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 2px; padding: 14px 8px; text-align: center;
  background: var(--panel-soft); color: var(--ink);
}
.dest-plate.b1 { background: var(--heat-1); color: var(--heat-1-ink); }
.dest-plate.b2 { background: var(--heat-2); color: var(--heat-2-ink); }
.dest-plate.b3 { background: var(--heat-3); color: var(--heat-3-ink); }
.dest-plate.b4 { background: var(--heat-4); color: var(--heat-4-ink); }
.dest-plate.b5 { background: var(--heat-5); color: var(--heat-5-ink); }
.dest-price {
  font-size: var(--t-h1); font-weight: 600; line-height: 1;
  letter-spacing: -0.015em; font-variant-numeric: tabular-nums;
}
.dest-body {
  padding: 14px 18px; min-width: 0;
  display: flex; flex-direction: column; justify-content: center;
}
.dest-where { margin: 0; font-size: var(--t-body); letter-spacing: -0.015em; }
.dest-where .muted {
  font-size: var(--t-micro); margin-left: 7px; letter-spacing: 0;
}
.dest-when { margin: 0; font-size: var(--t-small); color: var(--ink-soft); }
/* How live an Aviasales price is. --ink-soft, not --muted: it changes what
   the number means, so it has to be readable. */
.dest-seen { margin: 2px 0 0; font-size: var(--t-micro); color: var(--ink-soft); }
.dest-body .opt-toggle { align-self: flex-start; margin-top: 5px; }
.dest-act {
  padding: 14px 16px 14px 0;
  display: flex; flex-direction: column; align-items: flex-end;
  justify-content: center; gap: 7px;
}
.dest-airline { font-size: var(--t-micro); color: var(--ink-soft); font-weight: 500; }
.dest-act .book {
  display: block; width: 100%; text-align: center; padding: 9px 10px;
  font-size: var(--t-small); font-weight: 600; color: var(--on-accent);
  background: var(--accent); border-radius: var(--r-md); text-decoration: none;
}
.dest-act .book:hover { background: var(--accent-hover); text-decoration: none; }
.dest.over .book { background: var(--miss); }
.dest.over .book:hover { background: #3f4b58; }

/* A trip's other prices, opened from the row: the same card carrying on
   rather than a second thing under it. */
.dests .opt-detail {
  margin: 0; padding: 2px 18px 12px 92px; border-left: 0;
  background: var(--panel); border-radius: 0 0 var(--r-lg) var(--r-lg);
  box-shadow: 0 12px 28px -22px rgba(19, 26, 34, .55);
}
.fare:has(.opt-detail:not([hidden])) .dest {
  border-radius: var(--r-lg) var(--r-lg) 0 0; box-shadow: none;
}

/* The end of the list, and the way to the rest of it. The bar says how far
   down you are, because "show 14 more" on its own does not say what of. */
.dest-foot {
  margin-top: var(--s-4);
  display: flex; flex-direction: column; align-items: center; gap: var(--s-3);
}
/* The display above outranks the browser's own rule for `hidden`, so a list
   short enough to need no "show more" drew an empty bar and a blank button. */
.dest-foot[hidden] { display: none; }
.dest-foot p { margin: 0; }
.dest-meter {
  width: 100%; max-width: 420px; height: 4px; overflow: hidden;
  border-radius: var(--r-pill); background: var(--line);
}
.dest-meter i {
  display: block; height: 100%; border-radius: var(--r-pill);
  background: var(--accent); transition: width .3s ease;
}
.extend {
  padding: 10px 22px; font-size: var(--t-small); font-weight: 600;
  color: var(--ink); background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-pill); box-shadow: var(--shadow-sm);
}
.extend:hover { background: var(--panel); border-color: var(--ink); }
.dest-note { margin-top: var(--s-5); }

@media (max-width: 620px) {
  .opt { grid-template-columns: 88px minmax(0, 1fr); }
  .opt-note, .opt-link { grid-column: 2; text-align: left; }
  .opt-row { grid-template-columns: 78px minmax(0, 1fr); }
  .opt-row .opt-note, .opt-row .opt-link { grid-column: 2; text-align: left; }
  .listbar { border-radius: var(--r-lg); padding: 8px 10px; }
  .listbar-count { margin-left: 0; }
  /* The button goes under the trip rather than beside it, and stops being
     full-width with it: a phone-wide Book is a target, not a button. The
     plate still runs the height of the card - it is the edge of the card on
     this width, and half a plate over white reads as a broken one. */
  .dest { grid-template-columns: 78px minmax(0, 1fr); }
  .dest-plate { grid-row: 1 / span 2; }
  .dest-act {
    grid-column: 2; padding: 0 18px 14px;
    flex-direction: row; align-items: center;
    justify-content: flex-start; gap: var(--s-3);
  }
  .dest-act .book { width: auto; padding: 8px 18px; order: 2; }
  .dests .opt-detail { padding-left: 78px; }
}

/* This window's own price over time. The line is the decoration; the sentence
   under it is the content, which is why the svg is aria-hidden. */
.spark { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--line-soft); }
.spark-svg {
  display: block; width: 100%; height: 44px; overflow: visible;
}
.spark-line {
  fill: none; stroke: var(--accent); stroke-width: 2;
  stroke-linejoin: round; stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
.spark-limit {
  stroke: var(--ink); stroke-width: 1; stroke-dasharray: 3 3; opacity: .3;
  vector-effect: non-scaling-stroke;
}

.pill {
  display: inline-block; padding: 2px 10px; border-radius: var(--r-pill); font-size: var(--t-micro);
  background: var(--accent-soft); color: var(--accent);
}
.pill.off { background: var(--line-soft); color: var(--muted); }
.pill.hit { background: var(--hit-soft); color: var(--hit); font-weight: 500; }
.pill.wait { background: #f7ecd9; color: var(--warn); }

/* The price column in an alert table, and the label column beside it. Fixed,
   so the prices line up down the list rather than with each row's own text. */
.col-price { width: 96px; font-weight: 600; }
.col-label { width: 130px; font-weight: 600; }
/* An alert nobody has opened yet. */
tr.unread td { background: var(--panel-soft); }

/* The top of a tracker card: the route on the left, the price on the right. */
.card-top {
  display: flex; justify-content: space-between; gap: var(--s-5); flex-wrap: wrap;
}
/* The buttons in a page header, beside the title rather than under it. */
.row-actions { display: flex; gap: var(--s-3); align-items: flex-start; }

/* ---------- tables ---------- */
table { width: 100%; border-collapse: collapse; font-size: var(--t-small); }
th {
  text-align: left; font-weight: 500; color: var(--muted); font-size: var(--t-micro);
  letter-spacing: 0.04em; text-transform: uppercase;
  padding: 0 12px 10px 0; border-bottom: 1px solid var(--line);
}
td { padding: 11px 12px 11px 0; border-bottom: 1px solid var(--line-soft); vertical-align: top; }
tr:last-child td { border-bottom: none; }
td.num, th.num { text-align: right; padding-right: 0; font-variant-numeric: tabular-nums; }
/* Only the last column sits flush with the edge; interior numbers need air. */
td.num:not(:last-child), th.num:not(:last-child) { padding-right: 18px; }
tr.is-hit td { background: #f0f8f3; }

/* ---------- forms ---------- */
form.stack { display: block; }
fieldset {
  border: none; border-top: 1px solid var(--line-soft);
  padding: var(--s-5) 0 0; margin: var(--s-5) 0 0;
}
fieldset:first-of-type, fieldset.flush {
  border-top: none; padding-top: 0; margin-top: 0;
}
legend { display: none; }

.grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px 20px; }
.grid.three { grid-template-columns: repeat(3, 1fr); }
.grid.pair { grid-template-columns: 1fr 1fr; }   /* stays side by side on phones */
.grid .full { grid-column: 1 / -1; }

label.field { display: block; }
/* The same object as `.eyebrow` - a 12px uppercase label in the muted grey -
   and it was tracked at 0.07em where the eyebrow is tracked at 0.09em. The two
   sit six inches apart inside the route form's advanced panel, which is where
   a difference that small stops being invisible and starts looking like a
   mistake. */
label.field > span, .field-label {
  display: block; font-size: var(--t-micro); letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--muted); margin-bottom: var(--s-2);
}
.field-label.row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
}
.field-label .note {
  text-transform: none; letter-spacing: 0; font-size: var(--t-micro); color: var(--ink-soft);
  font-weight: 500;
}
input[type=text], input[type=email], input[type=password], input[type=number],
select {
  width: 100%; padding: 10px 12px; font: inherit; color: var(--ink);
  background: var(--field); border: 1px solid var(--line); border-radius: var(--r-md);
  transition: border-color .12s, box-shadow .12s;
}
/* Figures line up everywhere else in the app - the fares, the calendar, the
   price you are typing at. The two nights fields were the exception, and a
   7 beside a 10 in a proportional face is the one place it shows. */
input[type=number] { font-variant-numeric: tabular-nums; }
/* The chevron is drawn rather than inherited, so it is the one mark in the
   app that does not follow the palette by itself: its stroke is `--muted`
   written out by hand, here and in the dark block, and it has been left
   behind by a palette change before. */
select {
  appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path d='M1 1.5 6 6.5 11 1.5' fill='none' stroke='%235d6875' stroke-width='1.6' stroke-linecap='round'/></svg>");
  background-repeat: no-repeat; background-position: right 12px center;
  padding-right: 34px;
}
/* One focus ring, on everything that takes focus. What was here removed the
   outline from fields and replaced it with 3px of --accent-soft, which is
   1.1:1 against a white panel: the ring did no work and the whole indicator
   was a 1px change of border colour. Buttons and links, meanwhile, had no rule
   at all and fell back to whatever the browser drew. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
input:focus-visible, select:focus-visible { outline-offset: 1px; }
/* The field still lights up on a plain click; the ring above is what a
   keyboard gets, and it is now the thing you can actually see. */
input:focus, select:focus {
  border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft);
}
.hint { font-size: var(--t-micro); color: var(--muted); margin-top: var(--s-2); }

.checks { display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: center; }
/* The whole label is the target, so it is the label that has to clear the
   24px minimum - the box itself can stay the size it looks right at. */
.check { display: flex; align-items: center; gap: 8px; font-size: var(--t-small);
         min-height: 24px; }
.check input { accent-color: var(--accent); width: 16px; height: 16px;
               flex: none; }

.actions {
  display: flex; gap: 12px; align-items: center;
  margin-top: 24px; padding-top: 20px; border-top: 1px solid var(--line-soft);
}
.actions .spacer { margin-left: auto; }

button, .btn {
  font: inherit; padding: 10px 20px; border-radius: var(--r-md); cursor: pointer;
  border: 1px solid var(--accent); background: var(--accent); color: var(--on-accent);
  transition: background .12s, border-color .12s;
}
button:hover, .btn:hover { background: var(--accent-hover); text-decoration: none; }
.btn.ghost, button.ghost {
  background: var(--panel); color: var(--ink); border-color: var(--line);
}
.btn.ghost:hover, button.ghost:hover { background: var(--panel-soft); border-color: var(--line-strong); }
.btn.danger, button.danger {
  background: var(--panel); color: var(--danger); border-color: #e8d5d2;
}
.btn.danger:hover, button.danger:hover { background: #fdf6f5; }

/* ---------- the route form ---------- */
/* A panel that happens to hold a form, so it is built out of the same parts
   as every other panel: the panel shadow rather than the heavier one it had,
   24px of side padding like `.panel`, and a head band in flat `--panel-soft`
   like its own foot. The band used to be a white-to-lilac gradient, which put
   the only gradient-filled surface in the app at the top of the only page
   where nothing has been measured yet. */
.form-card {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-lg); box-shadow: var(--shadow-sm);
  max-width: 620px; margin: 0 auto; overflow: hidden;
}
.form-card .head {
  padding: var(--s-5) 24px;
  background: var(--panel-soft);
  border-bottom: 1px solid var(--line-soft);
}
/* The title of this page, at the size every other page sets its title - it
   was two steps down, so the one page in the app you reach by pressing a
   button labelled "New tracker" was also the one that whispered its name. */
.form-card .head h1 { margin: 0 0 var(--s-1); }
.form-card .head .lede { margin: 0; }
.form-card .body { padding: var(--s-5) 24px var(--s-1); }
.form-card .foot {
  display: flex; gap: var(--s-3); align-items: center;
  padding: var(--s-4) 24px; border-top: 1px solid var(--line-soft);
  background: var(--panel-soft);
}
.form-card .foot .spacer { margin-left: auto; }

/* One step between field groups, one step up from the 16px inside a `.grid`,
   so a row of two fields reads as one group rather than as two. */
.stack-gap > * + * { margin-top: var(--s-5); }

/* segmented control */
.seg {
  display: grid; grid-template-columns: 1fr 1fr; gap: 4px;
  background: var(--panel-soft); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 4px;
}
.seg label { display: block; cursor: pointer; user-select: none; }
.seg input { position: absolute; opacity: 0; width: 0; height: 0; }
.seg label > span {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 10px 12px; border-radius: var(--r-sm); font-size: var(--t-small); color: var(--muted);
  border: 1px solid transparent;
  transition: background .12s, color .12s, box-shadow .12s;
}
.seg label:hover > span { color: var(--ink); }
.seg input:checked + span {
  background: var(--panel); color: var(--ink); font-weight: 500;
  border-color: var(--line); box-shadow: var(--shadow-sm);
}
.seg input:focus-visible + span { box-shadow: 0 0 0 3px var(--accent-soft); }

/* airport combobox */
.combo { position: relative; }
.combo .box {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  min-height: 44px; padding: 6px 8px;
  background: var(--field); border: 1px solid var(--line); border-radius: var(--r-md);
  cursor: text; transition: border-color .12s, box-shadow .12s;
}
.combo.focus .box {
  border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft);
  outline: 2px solid var(--accent); outline-offset: 1px;
}
.combo .box input {
  flex: 1; min-width: 90px; border: none; padding: 4px 4px; font: inherit;
  background: transparent; outline: none; box-shadow: none;
}
/* The box above is already ringed; a second ring inside it is noise. */
.combo .box input:focus, .combo .box input:focus-visible {
  border: none; box-shadow: none; outline: none;
}
.chip {
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--accent-soft); color: var(--accent);
  border-radius: var(--r-sm); padding: 4px 6px 4px 9px; font-size: var(--t-small); max-width: 100%;
}
.chip b { font-weight: 600; letter-spacing: .02em; }
.chip .txt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chip button {
  border: none; background: none; color: inherit; opacity: .55;
  padding: 0 2px; font-size: var(--t-body); line-height: 1; cursor: pointer; border-radius: var(--r-sm);
}
.chip button:hover { opacity: 1; background: rgba(0, 0, 0, .06); }
.code-badge:empty { display: none; }
.code-badge {
  font-size: var(--t-micro); letter-spacing: .06em; font-weight: 600; color: var(--accent);
  background: var(--accent-soft); border-radius: var(--r-sm); padding: 1px 6px;
  text-transform: none;
}
.combo-menu {
  position: absolute; z-index: 30; left: 0; right: 0; top: calc(100% + 6px);
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-md);
  box-shadow: var(--shadow-md); max-height: 264px; overflow-y: auto; padding: 5px;
}
.combo-menu[hidden] { display: none; }
.combo-item {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 8px 9px; border-radius: var(--r-sm); cursor: pointer; text-align: left;
  border: none; background: none; color: inherit; font: inherit;
}
.combo-item:hover, .combo-item.on { background: var(--accent-soft); }
.combo-item .c {
  font-size: var(--t-micro); font-weight: 600; letter-spacing: .05em; color: var(--accent);
  background: var(--accent-soft); border-radius: var(--r-sm); padding: 3px 6px; min-width: 40px;
  text-align: center;
}
.combo-item.on .c, .combo-item:hover .c { background: var(--panel); }
.combo-item .t { min-width: 0; }
/* City and country share one line, as flex items rather than one run of text:
   the city truncates, the country never does. It is the only thing separating
   Barcelona ES from Barcelona VE, so it must survive a narrow field. */
.combo-item .t b { display: flex; gap: 6px; align-items: baseline; font-weight: 500; font-size: var(--t-small); }
.combo-item .t b .ci { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.combo-item .t b .co { flex: none; font-style: normal; font-weight: 400; color: var(--muted); }
.combo-item .t .nm {
  display: block; font-size: var(--t-micro); color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.combo-empty { padding: 12px 10px; font-size: var(--t-small); color: var(--muted); }

/* money input */
.money { position: relative; }
/* Set at the size of the figure it belongs to. At 14px against a 16px input
   the sign read as a label sitting next to the field rather than as the first
   character of the price. */
.money .cur {
  position: absolute; left: 13px; top: 50%; transform: translateY(-50%);
  color: var(--muted); pointer-events: none;
}
.money input { padding-left: 32px; font-variant-numeric: tabular-nums; }

/* ---------- the travel window ----------
   One field holding two dates, and one calendar behind it that draws the
   window between them. Day first and Monday first, because a native date
   input is neither in half the world and the format it chooses is not ours
   to pick. */
.daterange { position: relative; }
.dr-fields {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: stretch;
  background: var(--field); border: 1px solid var(--line); border-radius: var(--r-md);
  overflow: hidden; transition: border-color .12s, box-shadow .12s;
}
.daterange.open .dr-fields { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.dr-field {
  display: block; text-align: left; font: inherit; color: inherit;
  background: none; border: none; border-radius: 0; padding: 7px 14px 8px;
  cursor: pointer; transition: background .12s;
}
.dr-field:hover { background: var(--panel-soft); }
.dr-field[aria-expanded="true"], .dr-field[aria-expanded="true"]:hover {
  background: var(--accent-soft);
}
/* `--t-grid` is the seven-column month and nothing else. This is a field
   caption in a row of field captions, so it is set at the size they are. */
.dr-cap {
  display: block; font-size: var(--t-micro); letter-spacing: .09em;
  text-transform: uppercase; color: var(--muted);
}
.dr-val {
  display: block; font-size: var(--t-small); color: var(--ink);
  font-variant-numeric: tabular-nums; letter-spacing: .01em;
}
.dr-field.empty .dr-val { color: var(--muted); }
.dr-arrow { align-self: center; color: var(--muted); font-size: var(--t-small); }

.dr-pop {
  position: absolute; z-index: 30; top: calc(100% + 8px); left: 0;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: var(--shadow-md); padding: 15px 16px 12px;
}
.dr-pop[hidden] { display: none; }
.dr-nav {
  position: absolute; top: 13px; width: 28px; height: 28px; padding: 0;
  display: grid; place-items: center; border-radius: var(--r-sm);
  border: 1px solid var(--line); background: var(--panel); color: var(--ink-soft);
  font-size: var(--t-body); line-height: 1;
}
.dr-nav:hover { background: var(--panel-soft); color: var(--ink); }
.dr-nav[disabled] { opacity: .3; cursor: default; background: var(--panel); }
.dr-nav.prev { left: 16px; }
.dr-nav.next { right: 16px; }

.dr-months { display: flex; gap: 22px; }
.dr-month { width: 224px; }
.dr-mon {
  margin: 0 0 10px; text-align: center; font-weight: 600;
  font-size: var(--t-small); letter-spacing: -0.01em;
}
.dr-grid { display: grid; grid-template-columns: repeat(7, 1fr); }
.dr-dows { margin-bottom: 5px; }
.dr-dow {
  text-align: center; font-size: var(--t-grid); letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted);
}
/* The bar between the two dates is painted on the cells, not the buttons, so
   it runs unbroken across a week and stops half a cell short at each end. */
.dr-cell { display: flex; justify-content: center; padding: 1px 0; }
.dr-cell.in { background: var(--accent-soft); }
.dr-cell.from { background: linear-gradient(90deg, transparent 50%, var(--accent-soft) 50%); }
.dr-cell.to { background: linear-gradient(90deg, var(--accent-soft) 50%, transparent 50%); }
.dr-cell:nth-child(7n+1) { border-radius: var(--r-sm) 0 0 var(--r-sm); }
.dr-cell:nth-child(7n) { border-radius: 0 var(--r-sm) var(--r-sm) 0; }
/* A window that begins on a Sunday or ends on a Monday would otherwise hang
   half a cell of bar off the end of a row, pointing at nothing. */
.dr-cell.to:nth-child(7n+1), .dr-cell.from:nth-child(7n) { background: none; }
.dr-day {
  width: 30px; height: 30px; padding: 0; border: none; background: none;
  border-radius: 50%; color: var(--ink); font: inherit; font-size: var(--t-small);
  font-variant-numeric: tabular-nums; line-height: 30px;
  transition: background .1s, color .1s;
}
.dr-day:hover { background: rgba(var(--accent-rgb), .16); color: var(--ink); }
.dr-day[disabled] { color: #c3cad2; background: none; cursor: default; }
.dr-day.today { box-shadow: inset 0 0 0 1px var(--line); font-weight: 600; }
.dr-day.on, .dr-day.on:hover {
  background: var(--accent); color: var(--on-accent); font-weight: 600; box-shadow: none;
}
.dr-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  margin-top: 13px; padding-top: 11px; border-top: 1px solid var(--line-soft);
}
.dr-sum {
  margin: 0; font-size: var(--t-micro); color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.dr-done { padding: 6px 15px; font-size: var(--t-small); }

/* Two months will not sit beside each other on a phone, and the popover has
   to stay inside the card either way. */
@media (max-width: 620px) {
  .dr-pop { left: 0; right: 0; padding: 15px 14px 12px; }
  .dr-months { flex-direction: column; gap: 16px; }
  .dr-month { width: auto; }
  /* The columns are wider here than on a desktop and a 30px circle inside one
     is a small thing to hit with a thumb. */
  .dr-cell { padding: 2px 0; }
  .dr-day { width: 36px; height: 36px; line-height: 36px; }
}

/* advanced disclosure */
details.adv {
  border-top: 1px solid var(--line-soft); margin-top: var(--s-5); padding-top: var(--s-1);
}
details.adv > summary {
  list-style: none; cursor: pointer; padding: 14px 0;
  font-size: var(--t-small); color: var(--ink-soft);
  display: flex; align-items: center; gap: 8px;
}
details.adv > summary::-webkit-details-marker { display: none; }
details.adv > summary::before {
  content: ""; width: 7px; height: 7px; margin-left: 2px;
  border-right: 1.6px solid var(--muted); border-bottom: 1.6px solid var(--muted);
  transform: rotate(-45deg); transition: transform .15s;
}
details.adv[open] > summary::before { transform: rotate(45deg); }
details.adv > summary:hover { color: var(--ink); }
details.adv .inner { padding-bottom: 8px; }

/* One sentence in a tinted strip. It was laid out with flex and an 8px gap,
   which made every run of text between two <strong>s its own flex item: the
   figures came out spaced away from the words either side of them, and a full
   stop after a bolded figure sat 8px adrift of it. A sentence wants ordinary
   inline flow. */
.summary-line {
  margin: var(--s-5) 0 0;
  padding: var(--s-3) var(--s-4); border-radius: var(--r-md);
  background: var(--panel-soft); border: 1px solid var(--line-soft);
  font-size: var(--t-small); line-height: 1.55; color: var(--ink-soft);
}
.summary-line strong { font-weight: 600; color: var(--ink); }

/* ---------- price calendar ----------
   One cell per departure day. Cheaper days carry more of the navy, so the
   shape of a good month is visible before any number is read. Navy and not
   the green: this ramp ranks the days against each other, and on an airport
   page there is no limit at all. Green is kept for the one thing it means -
   a day under the limit - which `.hit` draws on top. */
.cal {
  display: grid; gap: 26px 30px;
  grid-template-columns: repeat(auto-fill, minmax(258px, 1fr));
}
.cal-label {
  font-size: var(--t-micro); font-weight: 600; letter-spacing: .02em;
  color: var(--ink-soft); margin: 0 0 8px;
}
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; }
.cal-dow {
  margin-bottom: 4px; font-size: var(--t-grid); letter-spacing: .06em;
  color: var(--muted); text-align: center;
}

.cal-day {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  min-height: 40px; padding: 3px 1px; border-radius: var(--r-sm);
  border: 1px solid transparent; font-variant-numeric: tabular-nums;
}
.cal-day b { font-size: var(--t-grid); font-weight: 500; color: var(--muted); line-height: 1.1; }
.cal-day i { font-size: var(--t-micro); font-style: normal; font-weight: 600; line-height: 1.3; }
.cal-day u { font-size: var(--t-grid); text-decoration: none; opacity: .78; line-height: 1; }
.cal-day.pad { min-height: 0; }
.cal-day.off b { opacity: .3; }
.cal-day.todo {
  border: 1px dashed var(--line); background: transparent;
}

/* cheapest fifth of the window through to the priciest */
.cal-day.l0 { background: #cbd6e6; color: #16233a; }
.cal-day.l1 { background: #dbe3ee; color: #24324a; }
.cal-day.l2 { background: #e7ecf2; color: #3a4757; }
.cal-day.l3 { background: #eff2f6; color: #525e6b; }
.cal-day.l4 { background: #f6f8fa; color: var(--muted); }
/* The day on the two darkest cells: the muted grey is under AA on them. */
.cal-day.l0 b, .cal-day.l1 b { color: var(--ink-soft); }
.cal-day.hit {
  border-color: var(--hit); box-shadow: inset 0 0 0 1px rgba(10, 107, 60, .25);
}
/* A step darker than --hit, which is 4.5:1 on the darkest cell and no more. */
.cal-day.hit i { color: #07552f; }

/* A day you can buy. Same lift as the heat map's own cells on a tracker page
   (`a.ad`), because it is the same promise: this number is a trip somebody is
   selling, and clicking it opens the place selling it. */
a.cal-day {
  text-decoration: none; cursor: pointer;
  transition: transform .08s ease, box-shadow .08s ease;
}
a.cal-day:hover { transform: translateY(-1px); box-shadow: 0 3px 10px rgb(19 26 34 / .18); }
a.cal-day:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

.cal-key {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--line-soft);
}
.cal-key .cal-day { min-height: 16px; width: 22px; padding: 0; }
.cal-key .k { font-size: var(--t-micro); color: var(--muted); margin: 0 8px 0 2px; }

/* ---------- notices ---------- */
/* A message you can put down. The x is the same size as a finger and quiet
   until it is under one: it is the way out of a sentence, not an action on
   the page. */
.notice {
  position: relative;
  padding: 11px 15px; border-radius: var(--r-md);
  margin-bottom: 20px; font-size: var(--t-small);
  background: var(--accent-soft); color: var(--accent);
}
.notice p { margin: 0; }
/* Only the one with a button gets the room for it: the others are prose that
   should keep the full width of the box. */
.notice-shut { padding-right: 46px; }
.notice-x {
  position: absolute; top: 6px; right: 7px; width: 28px; height: 28px;
  display: grid; place-items: center; padding: 0;
  font-size: var(--t-h2); line-height: 1; border-radius: 50%;
  border: 0; background: rgba(var(--accent-rgb), .08); color: inherit;
  box-shadow: none;
}
.notice-x:hover { background: rgba(var(--accent-rgb), .16); }
.notice.error { background: #fbeceb; color: #8f352c; }
/* Information rather than a warning: the accent-coloured box read as one. */
.notice.plain {
  background: var(--panel-soft); color: var(--ink-soft);
  border: 1px solid var(--line);
}
.notice.error .notice-x { background: rgba(143, 53, 44, .08); }
.notice.error .notice-x:hover { background: rgba(143, 53, 44, .16); }
/* The page in the visitor's own language: one sentence and two answers, on
   one line where there is room and stacked where there is not. The buttons
   are the small ones - this is an aside, not the page's call to action. */
.lang-hint {
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between; gap: var(--s-2) var(--s-4);
}
.lang-hint-act { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.lang-hint-act form { margin: 0; }
.lang-hint .btn, .lang-hint button { padding: 6px 14px; font-size: var(--t-small); }

.empty {
  text-align: center; padding: var(--s-7) var(--s-5); color: var(--muted);
  border: 1px dashed var(--line); border-radius: var(--r-lg);
  background: var(--panel-soft);
}
.empty p { margin: 0 0 var(--s-2); }
.empty p:last-child { margin-bottom: 0; }
.empty .btn { margin-top: var(--s-2); }

/* ---------- auth ---------- */
.auth { max-width: 380px; margin: 80px auto; }
.auth .panel { padding: 30px 30px 26px; }
.auth h1 { margin: 0 0 var(--s-5); }
.auth label.field { margin-bottom: 16px; }
.auth button { width: 100%; }
.auth .alt { text-align: center; margin-top: 18px; font-size: var(--t-small); color: var(--muted); }

/* ---------- landing ----------
   The front page is one route carried the whole way down: a card showing what
   a watched route looks like right now, and then that same route retold as
   four steps. Everything here is in service of the one worked example.

   .landing / .landing-title / .landing-lede are the plain centred page that
   predates the hero, and merge.html and message.html still use them. */
.landing { max-width: 640px; margin: 40px auto 0; }
.landing-title {
  font-size: var(--t-lg); font-weight: 600; letter-spacing: -0.03em;
  line-height: 1.2; margin: 0 0 16px;
}
.landing-lede { font-size: var(--t-lead); color: var(--ink-soft); margin: 0 0 30px; }
/* The front page sits on the app's own ground. What used to be here was two
   full-bleed radial washes, and the second of them was rgba(154, 106, 47, .075)
   - a warm brown, which is the hue this app's neutrals had before the ground
   turned periwinkle. It had been sitting behind the headline ever since. */
/* The hero is two columns, and 940px leaves the headline setting three words
   to a line next to the card. The front page alone gets the wider measure. */
.on-landing main .wrap,
.on-landing header.top .wrap { max-width: 1080px; }

.hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 420px);
  gap: var(--s-7);
  align-items: start;
  padding: 0 0 var(--s-7);
}
/* The headline is set in two lines and the break is where the emphasis lives.
   "every date" used to carry the accent colour and a highlighter rule as well
   - two devices doing one job, on the only `em` in the app that is styled at
   all, and the commonest tell of a page nobody wrote by hand. */
.hero-title {
  font-size: var(--t-xl); font-weight: 600; letter-spacing: -0.03em;
  line-height: 1.16; margin: 0 0 var(--s-3); text-wrap: balance;
}
/* One step up from body copy: it is carrying the whole explanation of the
   product on the one page where a visitor has read nothing else. */
.hero-lede { font-size: var(--t-lead); margin-bottom: var(--s-5); }
.hero-cta { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; }
/* No glow under this one. It is the same button as every other primary button
   in the app, and it was the only one with a coloured shadow. */
.hero-cta .btn { padding: 12px 22px; }

/* ---------- the worked example ---------- */
/* A panel with a head band - the same component the route form is - so it is
   built the same way: the system's own lift rather than a two-layer shadow of
   its own, and a flat band rather than the last gradient-filled panel header
   left in the app. */
.demo {
  margin: 0; background: var(--panel);
  border: 1px solid var(--line); border-radius: var(--r-lg); overflow: hidden;
  box-shadow: var(--shadow-md);
}
.demo-top {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-3);
  padding: var(--s-3) 18px; border-bottom: 1px solid var(--line-soft);
  background: var(--panel-soft);
}
.demo-tag {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: var(--t-micro); letter-spacing: .09em; text-transform: uppercase;
  font-weight: 600; color: var(--accent);
}
.demo-top-note { font-size: var(--t-micro); color: var(--muted); }
.demo-body { padding: 16px 18px; }
.demo-route { font-size: var(--t-h2); margin: 0 0 var(--s-2); }
.demo-meta { font-size: var(--t-micro); }
.demo-figures {
  display: flex; align-items: flex-end; justify-content: space-between; gap: var(--s-4);
  margin-top: var(--s-4); padding-top: var(--s-4); border-top: 1px solid var(--line-soft);
}
.demo-limit { text-align: right; }
.demo-limit .price-big { color: var(--ink-soft); }
.demo-strip-label { margin: var(--s-5) 0 var(--s-2); }
/* Not a month: a short run of consecutive departure days, so each cell carries
   its own weekday instead of sitting under a M-T-W header. */
.daystrip .cal-day { min-height: 43px; }
.daystrip .cal-day b { font-size: var(--t-grid); }
/* One line, whatever the place is called. A name that wraps takes its whole
   row's height with it, and a strip of unequal rows reads as a broken grid
   rather than as a calendar. `_cell_place()` cuts the long ones to a city and
   marks the cut with a hyphen; this is the backstop, and it keeps every cell
   the same height even when it fires. */
.daystrip .cal-day {
  /* A grid item's default `min-width: auto` lets its content push the column
     wider than its 1fr share, which with a one-line name meant the last
     column walking off the edge of the card. */
  min-width: 0;
}
.daystrip .cal-day u {
  display: block; max-width: 100%;
  /* A step under the day and the price, which is the order they are read in,
     and the step is what lets a ten-letter city sit inside a 48px cell
     without being cut at all. */
  font-size: 9px; letter-spacing: -.01em;
  white-space: nowrap; overflow: hidden;
  /* The cut is `_cell_place()`'s job and it marks it with a hyphen, which is
     what is normally seen. This is the backstop for the narrowest cards,
     where even a short name outruns 37px: a mark beats a word that simply
     stops mid-letter. */
  text-overflow: ellipsis;
}
.demo-note { margin-top: var(--s-3); }
.demo-key { margin-top: var(--s-4); padding-top: var(--s-3); gap: var(--s-2) var(--s-3); }
.demo-key .pair { display: inline-flex; align-items: center; gap: 5px; }
.demo-key .k { font-size: var(--t-micro); margin: 0; white-space: nowrap; }
.demo-key .ramp { display: inline-flex; gap: 2px; }
.demo .checking { margin: var(--s-4) 0 0; }

/* ---------- the four steps ---------- */
.flow-steps {
  list-style: none; margin: 0; padding: 0; position: relative;
  display: grid; grid-template-columns: repeat(4, 1fr);
}
/* One hairline through the row of numbers, each number punching a hole in it
   with a ring the colour of the page. */
.flow-steps::before {
  content: ""; position: absolute; left: 14px; right: 14px; top: 13px;
  height: 1px; background: var(--line);
}
/* Columns of unequal prose, but the thing each step is showing - the chips,
   the count, the bar, the email - lines up across the row. The 44px clears the
   27px number and the 5px ring around it; it is an offset, not a gap. */
.flow-steps > li {
  position: relative; padding: 44px var(--s-5) var(--s-1) 0;
  display: flex; flex-direction: column;
}
.flow-n {
  position: absolute; top: 0; left: 0; width: 27px; height: 27px;
  border-radius: 50%; display: grid; place-items: center;
  font-size: var(--t-micro); font-weight: 600; color: var(--on-accent); background: var(--accent);
  box-shadow: 0 0 0 5px var(--bg);
}
.flow-t { font-size: var(--t-body); font-weight: 600; letter-spacing: -0.01em; margin: 0 0 var(--s-2); }
.flow-b { margin: 0 0 var(--s-4); flex: 1 0 auto; }
.flow-fig {
  display: flex; flex-direction: column; margin: 0; color: var(--accent);
  font-size: var(--t-lg); font-weight: 600; letter-spacing: -0.015em;
  font-variant-numeric: tabular-nums;
}
.flow-fig small {
  font-size: var(--t-micro); font-weight: 400; letter-spacing: 0; color: var(--muted);
  margin-top: var(--s-1);
}
.flow-bar { margin-top: var(--s-2); }
.flow-under { margin: var(--s-2) 0 0; }

.chips { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.chip-flat {
  font-size: var(--t-micro); padding: 4px 10px; border-radius: var(--r-pill);
  background: var(--panel); border: 1px solid var(--line); color: var(--ink-soft);
}
.chip-flat.strong {
  background: var(--hit-soft); border-color: var(--hit-line);
  color: var(--hit); font-weight: 500;
}

/* the alert, as it arrives */
.mail {
  background: var(--panel); border: 1px solid var(--line);
  border-left: 3px solid var(--hit); border-radius: var(--r-md);
  padding: 12px 14px; box-shadow: var(--shadow-sm);
}
.mail-from {
  font-size: var(--t-grid); letter-spacing: .09em; text-transform: uppercase;
  color: var(--muted); margin: 0 0 5px;
}
.mail-sub { font-size: var(--t-small); font-weight: 600; letter-spacing: -0.01em; margin: 0 0 3px; }
.mail-body { font-size: var(--t-micro); color: var(--muted); margin: 0; }

/* The sticky header would otherwise land on top of whatever was jumped to. */
#how, #start { scroll-margin-top: 78px; }

/* The closing CTA. It used to be the heading of the route list, so it sat
   tight against the four steps above it; standing on its own it needs the
   rule and the room to read as a separate offer rather than a fifth step. */
.landing-cta {
  align-items: center; margin-top: var(--s-7); padding-top: var(--s-6);
  border-top: 1px solid var(--line-soft);
}

@media (max-width: 900px) {
  .hero { grid-template-columns: 1fr; gap: var(--s-6); padding-bottom: var(--s-6); }
  .demo { max-width: 460px; }
  .hero-title { font-size: var(--t-lg); }
}
@media (max-width: 860px) {
  .flow-steps { grid-template-columns: repeat(2, 1fr); gap: var(--s-4) 0; }
  .flow-steps::before { display: none; }
}
@media (max-width: 520px) {
  .flow-steps { grid-template-columns: 1fr; gap: var(--s-5) 0; }
  .hero-title { font-size: var(--t-lg); }
  .hero-cta .btn { width: 100%; text-align: center; }
}

/* ---------- moving trackers into an account ----------
   One line per tracker on the merge offer. Deliberately not .opt: that grid
   has fixed columns sized for prices and dates, and a route name folded onto
   three lines inside its 92px first column. */
.move-row {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 8px 16px; flex-wrap: wrap;
  padding: 10px 0; font-size: var(--t-small);
}
.move-row + .move-row { border-top: 1px solid var(--line-soft); }
.move-row .r { font-weight: 600; }
.move-row .w { color: var(--muted); font-size: var(--t-small); }

/* ---------- copy row ---------- */
.copy-row { display: flex; gap: 10px; }
.copy-row input {
  flex: 1; font-family: var(--font-num);
  font-size: var(--t-small); background: var(--panel-soft);
}
.copy-row button { white-space: nowrap; }

/* An address and a button on one line: the tracker page's "email me" and an
   airport page's one-line alert. Wraps rather than squeezing on a phone. */
.inbox-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: var(--s-2); }
.inbox-row > input[type=email] { flex: 1 1 14rem; min-width: 0; }
.inbox-row > .money { flex: 0 1 8rem; }
.inbox-row > button { white-space: nowrap; }
.airport-alert { margin-top: var(--s-5); max-width: 36rem; }

/* The soft-accent pills are links in a couple of places; the grey ones are
   too, and used to take an accent hover whatever colour they started. */
a.pill:hover { text-decoration: none; background: rgba(var(--accent-rgb), .16); }
a.pill.off:hover { background: var(--line); color: var(--ink-soft); }

input:disabled, .check input:disabled { opacity: .45; cursor: not-allowed; }
.check input:disabled + * { color: var(--muted); }

/* ---------- toast ---------- */
#toast {
  position: fixed; right: 22px; bottom: 22px; width: 320px; z-index: 50;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: 0 8px 28px rgba(20, 22, 26, .12);
  padding: 16px 18px; display: none;
}
#toast .eyebrow { margin-bottom: 6px; }
#toast .t { font-size: var(--t-lead); font-weight: 600; margin-bottom: 3px; }
#toast .b { font-size: var(--t-small); color: var(--muted); }
#toast .x {
  position: absolute; top: 10px; right: 12px; color: var(--muted);
  cursor: pointer; font-size: var(--t-body); line-height: 1; border: none; background: none; padding: 0;
}

footer.foot {
  border-top: 1px solid var(--line); padding: var(--s-5) 0 var(--s-6);
  color: var(--muted); font-size: var(--t-micro);
  /* main's own 80px of bottom padding was sized for a page that ended in
     whitespace. With something under it that reads as the end of the page,
     the gap is the footer's to own. */
  margin-top: var(--s-5);
}
.foot-row {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: var(--s-4) var(--s-6);
}
.foot-links { display: flex; gap: var(--s-4); flex-wrap: wrap; flex: 0 0 auto; }
.foot-links a { color: var(--muted); }
.foot-links a:hover, .foot-links a.active { color: var(--ink-soft); }
/* The language the page is in is named, not linked: the one you are reading. */
.foot-langs span[aria-current] { color: var(--ink-soft); }
.foot-meter {
  margin: var(--s-4) 0 0; padding-top: var(--s-3);
  border-top: 1px solid var(--line-soft);
}
@media (max-width: 620px) {
  .foot-row { flex-direction: column; gap: var(--s-4); }
}

/* ---------- a page that is only prose ----------
   The privacy page, and anything like it. Every other page here is panels and
   figures, so the base sheet has no rules for a run of headings and
   paragraphs at all - h2 and h3 sat almost on top of the text above them. */
.prose { max-width: var(--measure); }
.prose h2 { margin-top: var(--s-7); }
.prose h3 {
  font-size: var(--t-body); font-weight: 600; letter-spacing: -0.01em;
  margin: var(--s-5) 0 var(--s-2);
}
.prose p { margin: 0 0 var(--s-4); }
.prose ul { margin: 0 0 var(--s-4); padding-left: 1.15em; }
.prose li { margin-bottom: var(--s-2); }
.prose li:last-child { margin-bottom: 0; }

@media (max-width: 700px) {
  .landing-title { font-size: var(--t-lg); }
  .grid, .grid.three { grid-template-columns: 1fr; }
  .grid.pair { grid-template-columns: 1fr 1fr; gap: 12px; }
  /* Six links do not fit beside the wordmark on a phone. In one row at 375px
     the page ran 95px wider than the screen and took Settings and Sign in off
     the right-hand edge with it - and Settings is where a guest's recovery
     link lives, so the one thing they cannot afford to lose was the one thing
     they could not reach. The nav drops to its own line, and scrolls sideways
     rather than pushing the page wide if even that is not enough. */
  header.top .wrap {
    height: auto; flex-wrap: wrap; gap: 0; padding-top: 11px; padding-bottom: 9px;
  }
  nav.main {
    width: 100%; margin-left: 0; gap: 14px; padding: 9px 0 10px;
    flex-wrap: nowrap; overflow-x: auto;
    scrollbar-width: none; -ms-overflow-style: none;
  }
  /* The sideways scroll above makes this row a clipping box, and the marker
     under the current page hangs below the text - without the padding it is
     cut off and the row loses the one thing that says where you are. */
  nav.main a.active::after { bottom: -5px; }
  nav.main::-webkit-scrollbar { display: none; }
  nav.main a { white-space: nowrap; }
  /* The Guest pill is the widest thing in the row and the only one that is
     not a destination - it says what Settings says, through a tooltip no
     touchscreen shows. Dropping it here is what lets the rest fit outright
     rather than relying on the sideways scroll above. */
  nav.main a.pill { display: none; }
  /* The switch stays up beside the wordmark rather than taking a line of
     its own under the nav: it is the one thing in the header that is not a
     link, and the nav row is already as full as a phone allows. */
  nav.main { order: 1; }
  .theme-toggle { margin-left: auto; }
  .wrap { padding: 0 16px; }
  .form-card .body, .form-card .head, .form-card .foot { padding-left: 18px; padding-right: 18px; }
}

/* Visible to a screen reader and to nothing else. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* The skip link: above the page and out of sight until a keyboard reaches
   it, then over the header's left edge, where the eye already is. */
.skip {
  position: absolute; left: 16px; top: -64px; z-index: 70;
  padding: 10px 16px; border-radius: var(--r-md);
  background: var(--accent); color: var(--on-accent);
  font-weight: 600; text-decoration: none;
}
.skip:focus { top: 10px; }
/* Where it lands. Focusable, so the next Tab starts inside the page, but not
   a control, so no ring round the whole of it. */
main:focus { outline: none; }

/* ---------- "checking now" ----------
   Every slow moment in this app is the same thing: a SerpApi search is in
   flight and the answer is seconds away. One strip says so - a plane drawing
   a line between the two airport codes, at a walking pace. The pace is the
   point: a busy, twitchy animation makes a fifteen-second wait feel longer
   than it is, so nothing here moves faster than one crossing every 3.4s. */
.checking {
  display: flex; gap: 18px; align-items: center;
  background: var(--panel-soft);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 15px 20px 16px;
  margin-bottom: 18px;
  box-shadow: var(--shadow-sm);
}
.checking[hidden] { display: none; }
.checking .body { flex: 1 1 auto; min-width: 0; }
.checking .eyebrow { margin: 0 0 9px; color: var(--accent); }
.checking .note { margin: 9px 0 0; }

/* the ellipsis after "Checking now" - three dots taking their turn */
.ell i { font-style: normal; opacity: .25; animation: ell 1.5s infinite; }
.ell i:nth-child(2) { animation-delay: .25s; }
.ell i:nth-child(3) { animation-delay: .5s; }
@keyframes ell { 0%, 65%, 100% { opacity: .25; } 25% { opacity: 1; } }

/* the flight line */
.flightline {
  display: flex; align-items: center; gap: 12px;
  font-size: var(--t-micro); font-weight: 600; letter-spacing: .06em;
  color: var(--ink-soft);
}
.flightline .code { flex: 0 0 auto; }
.flightline .path {
  position: relative; flex: 1 1 auto; height: 18px; min-width: 60px;
}
/* the route not yet flown: a dotted hairline, drawn with a gradient so the
   dots keep their spacing at any width */
.flightline .path::before {
  content: ""; position: absolute; left: 0; right: 0; top: 50%; height: 2px;
  transform: translateY(-50%); border-radius: var(--r-sm);
  background-image: radial-gradient(circle, var(--line) 1px, transparent 1px);
  background-size: 6px 2px; background-repeat: repeat-x;
}
/* the part already flown */
.flightline .drawn {
  position: absolute; left: 0; top: 50%; height: 2px; width: 0;
  transform: translateY(-50%); border-radius: var(--r-sm);
  background: linear-gradient(90deg, rgba(var(--accent-rgb), .18), var(--accent));
  animation: draw 3.4s cubic-bezier(.45, 0, .4, 1) infinite;
}
.flightline .plane {
  position: absolute; left: 0; top: 50%; margin-top: -7px; margin-left: -7px;
  width: 14px; height: 14px; color: var(--accent);
  animation: fly 3.4s cubic-bezier(.45, 0, .4, 1) infinite;
}
.flightline .plane svg { display: block; width: 14px; height: 14px; animation: bob 3.4s ease-in-out infinite; }

/* Both runs stay level for the last 12% and then fade, so the loop reads as
   "landed, and off again" rather than a rubber band snapping back. */
@keyframes fly {
  0%   { left: 0;    opacity: 0; }
  7%   { opacity: 1; }
  86%  { left: 100%; opacity: 1; }
  95%  { left: 100%; opacity: 0; }
  100% { left: 100%; opacity: 0; }
}
@keyframes draw {
  0%   { width: 0;    opacity: 0; }
  7%   { opacity: 1; }
  86%  { width: 100%; opacity: 1; }
  95%  { width: 100%; opacity: 0; }
  100% { width: 100%; opacity: 0; }
}
@keyframes bob {
  0%, 100% { transform: translateY(0); }
  45%      { transform: translateY(-2px); }
}

/* ---------- what the scan is doing ----------
   The strip above says a search is in flight. This says how far it has got
   and what it has cost: a count, the cheapest price so far, and a rail of one
   segment per source, in the order they are asked.

   What was here was a row per airline - name, then what it said - and on a
   normal run four of the six rows read "no answer" or "nothing in your
   window". It was an accurate report on the plumbing and a miserable one on
   the trip: the traveller cannot make Wizz Air answer, so naming Wizz Air
   only says "failed" twice as loudly as the fares that did land say "found".
   The rail carries the same coverage - six segments, four of them filled -
   with none of the vocabulary, and the one thing a wait needs, which is a
   number that moves. */
.checking .sweep {
  margin: 12px 0 0; padding: 11px 0 0;
  border-top: 1px solid var(--line);
}
.checking .sweep[hidden] { display: none; }
.sweep-head { display: flex; align-items: center; gap: 11px; }
.sweep-say { min-width: 0; }
.sweep-said {
  margin: 0; font-size: var(--t-small); font-weight: 600; color: var(--ink);
  letter-spacing: -0.01em;
}
.sweep-meta { margin: 1px 0 0; font-size: var(--t-micro); color: var(--muted); }
.sweep-meta[hidden] { display: none; }
/* Mid-sweep the line is a running count, not a result, so it is set as one. */
.sweep:not(.is-done) .sweep-said { font-weight: 400; color: var(--ink-soft); }

/* The tick draws itself once, at the end, and then holds still. It is the
   only thing on the strip that says "over" rather than "going". */
.sweep-tick { flex: 0 0 auto; width: 26px; height: 26px; display: none; }
.sweep.is-done .sweep-tick { display: block; }
.sweep-tick circle {
  fill: none; stroke: var(--accent); stroke-width: 2;
  stroke-dasharray: 88; stroke-dashoffset: 88;
  animation: tick-ring .55s cubic-bezier(.4, 0, .2, 1) both;
}
.sweep-tick path {
  fill: none; stroke: var(--accent); stroke-width: 2.6;
  stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 22; stroke-dashoffset: 22;
  animation: tick-mark .3s cubic-bezier(.4, 0, .2, 1) .38s both;
}
@keyframes tick-ring { to { stroke-dashoffset: 0; } }
@keyframes tick-mark { to { stroke-dashoffset: 0; } }

/* The rail. Segments are not labelled and are not meant to be counted off
   against airlines - they are how much of the sweep is in. Each one fills
   when its source lands, so the stagger is the run itself rather than an
   animation pretending to be one. */
.sweep-rail { display: flex; gap: 3px; margin: 12px 0 0; }
.sweep-rail i {
  flex: 1 1 0; height: 6px; border-radius: var(--r-sm);
  background: var(--line);
  transition: background-color .35s ease;
}
/* Answered. Not "found something" - answered. A source with nothing on this
   route, one that refused and one that never replied all land here, because
   the alternative is a finished run wearing four grey segments, and grey is
   what a segment nobody has reached yet looks like. See `sweep` in app.js. */
.sweep-rail i.got {
  background: linear-gradient(90deg, rgba(var(--accent-rgb), .55), var(--accent));
}
.sweep-rail i.asking { background: var(--accent); animation: pulse 1.2s ease-in-out infinite; }

@keyframes pulse { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }

/* The one sentence for the whole run, once it is over. */
.steps-sum { margin: 11px 0 0; color: var(--ink-soft); }
.steps-sum[hidden] { display: none; }

/* A finished run is not a run in flight: the plane stops, and the strip reads
   as what just happened rather than as something still happening. */
.checking.settled .flightline,
.checking.settled .ell { display: none; }
.checking.settled { background: var(--panel); }

/* A region that has just been replaced with fresher fares. Long enough to
   catch the eye that was looking somewhere else on the page, short enough
   that a scan answering six times does not leave the page blinking. */
.just-in > * { animation: just-in 1.4s ease-out 1; }
@keyframes just-in {
  0%   { background-color: var(--accent-soft); }
  100% { background-color: transparent; }
}

/* a button that has just been pressed and is waiting on the network */
.is-busy { pointer-events: none; opacity: .75; }
.spin {
  display: inline-block; width: 11px; height: 11px; margin-right: 8px;
  vertical-align: -1px; border-radius: 50%;
  border: 2px solid currentColor; border-top-color: transparent;
  opacity: .8; animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* the hairline at the very top of the window, for the whole-page waits */
#busybar {
  position: fixed; top: 0; left: 0; right: 0; height: 2px;
  z-index: 60; overflow: hidden; display: none;
}
#busybar span {
  display: block; height: 100%; width: 38%; border-radius: 0 var(--r-sm) var(--r-sm) 0;
  background: linear-gradient(90deg, rgba(var(--accent-rgb), .15), var(--accent));
  animation: sweep 1.5s cubic-bezier(.5, 0, .3, 1) infinite;
}
@keyframes sweep {
  0%   { transform: translateX(-105%); width: 38%; }
  55%  { width: 62%; }
  100% { transform: translateX(165%);  width: 32%; }
}

@media (max-width: 520px) {
  .checking { padding: 14px 16px 15px; }
  .flightline { gap: 9px; font-size: var(--t-micro); }
}

/* Nothing here carries meaning that the words do not, so it can all stop. */
@media (prefers-reduced-motion: reduce) {
  .flightline .plane, .flightline .plane svg, .ell i, .spin, #busybar span,
  .sweep-rail i.asking, .just-in > * {
    animation: none;
  }
  /* The tick is drawn by its own animation, so stopping it without landing
     it on the finished shape would leave the strip with no mark at all. */
  .sweep-tick circle, .sweep-tick path { animation: none; stroke-dashoffset: 0; }
  .sweep-rail i { transition: none; }
  .flightline .drawn { animation: none; width: 100%; opacity: .3; }
  .flightline .plane { left: 100%; opacity: .5; }
  #busybar span { width: 100%; }
}

/* ---------- the dashboard, read as a window ----------------------------
   Two columns and a month grid need more room than the 940px a page of
   prose wants, and the header and footer read the same variable, so the
   whole page widens together rather than the content sliding out from
   under the nav. */
body.atlas { --maxw: 1080px; }

/* One chip per tracker, carrying a dot in its cheapest fare's band. Which
   tracker is worth opening is then legible before it is opened, which is
   the thing a row of identical names could never say. */
.tt-row { display: flex; gap: var(--s-2); flex-wrap: wrap; margin-bottom: var(--s-3); }
.tt {
  display: inline-flex; align-items: center; gap: var(--s-2);
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-pill);
  padding: 7px 15px; font: inherit; font-size: var(--t-small);
  color: var(--ink-soft); cursor: pointer;
}
.tt:hover { border-color: var(--line-strong); }
.tt.on { background: var(--ink); color: var(--panel); border-color: var(--ink); }
/* Hangs under the chip row, on the far side of it, so the chips read left to
   right and the way in sits at the end of them. It is the first thing in the
   panel rather than a row of its own above the panels, which is what keeps it
   pointing at the tracker actually on screen. */
.panel-open {
  display: flex; justify-content: flex-start;
  margin: 0 0 var(--s-4); padding-left: 2px; font-size: var(--t-small);
}
.panel-open a { font-weight: 600; }
.tt.on .muted, .tt.on .arrow { color: rgba(255, 255, 255, .68); }
.tt:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.tt-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
.tt-dot.b1 { background: var(--heat-1); }
.tt-dot.b2 { background: var(--heat-2); }
.tt-dot.b3 { background: var(--heat-3); }
.tt-dot.b4 { background: var(--heat-4); }
.tt-dot.b5 { background: var(--heat-5); }
.tt-dot.bnone, .tt-dot.off { background: transparent; box-shadow: inset 0 0 0 1.5px #aab4be; }
/* The palest three are swatches, not marks: at 9px on a white chip they all
   but vanish. A ring keeps each one a dot - the palest green ringed in the
   middle one, so it still reads as green. */
.tt-dot.b3 { box-shadow: inset 0 0 0 1.5px var(--heat-2); }
.tt-dot.b4, .tt-dot.b5 { box-shadow: inset 0 0 0 1.5px #aab4be; }

/* The opener says the one thing worth saying about this tracker today, and
   beside it the cheapest day at the size the decision deserves - the same
   shape as a cell in the grid below, in the same colour. */
.opener {
  background: var(--blush); border-radius: var(--r-lg); padding: 26px 28px;
  display: grid; grid-template-columns: 1fr 258px; gap: 30px; align-items: center;
  margin-bottom: var(--s-5);
}
/* Green says a fare is under the limit, so the opener only takes the wash
   when it is saying that. Paused, nothing under yet, or nothing found: the
   plain panel, and the card beside it carries whatever colour the fare is. */
.opener.quiet { background: var(--panel); border: 1px solid var(--line); }
.opener.quiet .words p { color: var(--ink-soft); }
/* No max-width on either: the grid column is already a 44-character measure
   at this size, and a `ch` cap here resolved against the inherited 16px font
   rather than the 30px one, which cut the headline to a third of its column
   and broke a two-line sentence over three. */
.opener .words h2 {
  margin: 0 0 var(--s-2); font-size: var(--t-lg); line-height: 1.12;
  letter-spacing: -0.025em; text-wrap: balance;
}
/* `em`, not `ch` - see above. 36em at 14px is a 57-character measure. */
.opener .words p { margin: 0; font-size: var(--t-small); color: var(--blush-ink); max-width: 36em; }

.pick { border-radius: var(--r-lg); padding: 17px 19px 19px; }
.pick.b1 { background: var(--heat-1); color: var(--heat-1-ink); }
.pick.b2 { background: var(--heat-2); color: var(--heat-2-ink); }
.pick.b3 { background: var(--heat-3); color: var(--heat-3-ink); }
.pick.b4 { background: var(--heat-4); color: var(--heat-4-ink); }
.pick.b5 { background: var(--heat-5); color: var(--heat-5-ink); }
.pick-day { margin: 0; font-size: var(--t-micro); opacity: .85; }
.pick-fare {
  margin: 2px 0 0; font-size: var(--t-xl); font-weight: 600; line-height: 1;
  letter-spacing: -0.015em; font-variant-numeric: tabular-nums;
}
.pick-where { margin: 7px 0 0; font-size: var(--t-body); font-weight: 600; }
.pick-note { margin: 1px 0 var(--s-4); font-size: var(--t-micro); opacity: .85; }
.pick-go {
  display: block; text-align: center; background: var(--panel); color: var(--ink);
  border-radius: var(--r-md); padding: 9px; font-size: var(--t-small); font-weight: 600;
}
/* Written in the green on any card under the limit. The card's own fill
   would not do: the middle green is 2.9:1 on white and the palest is a
   swatch, not an ink. */
.pick.b1 .pick-go, .pick.b2 .pick-go, .pick.b3 .pick-go { color: var(--hit); }
.pick.b4 .pick-go, .pick.b5 .pick-go { background: var(--ink); color: var(--panel); }
.pick-go:hover { text-decoration: none; opacity: .88; }

.atlas-body { display: grid; grid-template-columns: 1fr 300px; gap: var(--s-5); align-items: start; }
.cal-panel, .atlas-rail {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--r-lg); padding: 18px 20px 16px;
}
.cal-head { margin-bottom: var(--s-4); }
.cal-head h3 { margin: 0 0 2px; font-size: var(--t-body); font-weight: 600; letter-spacing: -0.015em; }

/* Months stack rather than sitting side by side: a seven-column grid holding
   a fare and a city name in each cell stops being readable somewhere around
   300px, and a two-month window would put both of them there. */
.atlas-cal { display: grid; gap: var(--s-5); }
.atlas-note { margin-top: var(--s-3); }
.atlas-mlabel { margin: 0 0 var(--s-2); font-size: var(--t-small); font-weight: 600; }
/* minmax(0, 1fr), not 1fr. A grid column's default minimum is its content,
   so one cell holding "Gdansk Lech Walesa" widened its whole column and every
   other day of that weekday with it - the grid drifted out of square by
   however long the longest city name was. Zero minimum pins the seven columns
   equal and leaves the name to ellipsis inside its own cell. */
.atlas-dow, .atlas-days { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 5px; }
.atlas-dow { font-size: var(--t-grid); color: var(--muted); margin-bottom: 5px; }
.atlas-dow span { text-align: center; }
/* One fixed size for every day of the grid. Height as well as width: a cell
   is a swatch on a heat map, and a swatch that grows because its fare has a
   longer city name under it reads as a difference in the data. */
.ad {
  display: flex; flex-direction: column; gap: 1px; height: 66px; min-width: 0;
  padding: 7px 8px 8px; border-radius: var(--r-md);
  background: var(--panel-soft); color: var(--muted);
  font-variant-numeric: tabular-nums; overflow: hidden; line-height: 1.2;
}
/* A day that can be booked is a link into that trip, and looks like the rest
   of the grid until you point at it. */
a.ad { text-decoration: none; cursor: pointer; transition: transform .08s ease, box-shadow .08s ease; }
a.ad:hover { transform: translateY(-1px); box-shadow: 0 3px 10px rgb(19 26 34 / .18); }
a.ad:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
/* No shrinking either. The cell is a fixed box now, so a flex child left
   free to give way had the city name squeezed to half a line of itself
   rather than the date and the fare making room for it. */
.ad > * { min-width: 0; flex: none; }
.ad b { font-size: var(--t-micro); font-weight: 500; opacity: .75; }
.ad i { font-size: var(--t-small); font-style: normal; font-weight: 600; letter-spacing: -0.02em; }
.ad u {
  font-size: var(--t-grid); text-decoration: none; opacity: .92; line-height: 1.2;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ad.pad { background: transparent; }
.ad.todo { background: transparent; border: 1px dashed var(--line); }
/* Hatched, not blank. A day whose shortest return would land after the window
   closes is not a day with no fare found - it was never watchable, and a grid
   that draws the two the same way invites the same question every time. */
.ad.gone, .ad.late, .ad.skipped {
  background: repeating-linear-gradient(135deg,
    #f7f9fa, #f7f9fa 4px, #eaeef1 4px, #eaeef1 8px);
  color: var(--muted);
}
.ad.b1 { background: var(--heat-1); color: var(--heat-1-ink); }
.ad.b2 { background: var(--heat-2); color: var(--heat-2-ink); }
.ad.b3 { background: var(--heat-3); color: var(--heat-3-ink); }
.ad.b4 { background: var(--heat-4); color: var(--heat-4-ink); }
.ad.b5 { background: var(--heat-5); color: var(--heat-5-ink); }
.ad.best { box-shadow: 0 0 0 2px var(--ink); }

.atlas-key {
  display: flex; align-items: center; gap: 3px; flex-wrap: wrap;
  margin-top: var(--s-5); padding-top: var(--s-4); border-top: 1px solid var(--line-soft);
  font-size: var(--t-micro); color: var(--muted); font-variant-numeric: tabular-nums;
}
.atlas-key i { display: block; width: 30px; height: 9px; border-radius: 2px; }
.atlas-key i.b1 { background: var(--heat-1); }
.atlas-key i.b2 { background: var(--heat-2); }
.atlas-key i.b3 { background: var(--heat-3); }
.atlas-key i.b4 { background: var(--heat-4); }
.atlas-key i.b5 { background: var(--heat-5); }
.atlas-key .k { margin: 0 var(--s-2); }
.atlas-key .turn {
  display: flex; align-items: center; gap: 5px; margin: 0 7px;
  color: var(--ink); font-weight: 600;
}
.atlas-key .turn s {
  display: block; width: 1px; height: 15px; background: var(--ink); text-decoration: none;
}

.atlas-rail h3 { margin: 0 0 2px; font-size: var(--t-small); font-weight: 600; }
/* A price keeps its colour wherever it is shown, so the plate here and the
   cell in the grid are the same fare said twice, not two facts to reconcile. */
/* `inline-block` with a min-width as well as the flex basis, because this is
   used in two places: a flex row on the dashboard, and a table cell on the
   alerts page where a bare span would have taken the padding and none of the
   width. Same plate either way, so a fare is the same size wherever it is. */
.plate {
  display: inline-block; min-width: 62px; flex: 0 0 62px;
  text-align: center; border-radius: var(--r-sm); padding: 5px 0;
  font-size: var(--t-small); font-weight: 600; letter-spacing: 0;
  font-variant-numeric: tabular-nums;
}
.plate.b1 { background: var(--heat-1); color: var(--heat-1-ink); }
.plate.b2 { background: var(--heat-2); color: var(--heat-2-ink); }
.plate.b3 { background: var(--heat-3); color: var(--heat-3-ink); }
.plate.b4 { background: var(--heat-4); color: var(--heat-4-ink); }
.plate.b5 { background: var(--heat-5); color: var(--heat-5-ink); }
.plate.small-plate { flex-basis: 52px; padding: 3px 0; font-size: var(--t-micro); }

.rail-row {
  display: flex; align-items: center; gap: 10px; padding: 9px 0;
  border-top: 1px solid var(--line-soft);
}
.rail-what { flex: 1 1 auto; min-width: 0; font-size: var(--t-small); font-weight: 500; }
.rail-what span { display: block; font-size: var(--t-micro); color: var(--muted); font-weight: 400; }
.rail-row.over .rail-what { color: var(--muted); }
.rail-go { flex: none; font-size: var(--t-micro); }
.rail-cut {
  display: flex; align-items: center; gap: 10px; margin: var(--s-4) 0 0;
  font-size: var(--t-micro); color: var(--muted);
}
.rail-cut::before, .rail-cut::after { content: ""; height: 1px; background: var(--line); flex: 1; }
.rail-since { margin-top: var(--s-5); padding-top: var(--s-3); border-top: 1px solid var(--line); }
.rail-ev { display: flex; align-items: center; gap: 10px; padding: 4px 0; font-size: var(--t-micro); }
.rail-ev > span:last-child { color: var(--muted); }

@media (max-width: 880px) {
  .opener, .atlas-body { grid-template-columns: 1fr; }
  .opener { gap: var(--s-5); }
}
@media (max-width: 620px) {
  .ad { height: 52px; padding: 5px 5px 6px; }
  .ad u { display: none; }
  .ad i { font-size: var(--t-micro); }
  .atlas-dow span { font-size: 9px; }
}

/* ---------- the airport pages ----------
   The front page's hero and card, with a table of destinations under them, so
   almost everything here is borrowed. These are the joins. */
.airport-figures { margin: 0 0 var(--s-5); }
.airport-dests, .airport-others { margin-top: var(--s-7); }
.airport-others .chip-flat:hover { border-color: var(--line-strong); text-decoration: none; }
.demo-more { margin: var(--s-4) 0 0; }
.demo-more a { font-weight: 600; }

/* ---------- admin ---------- */
/* The operator's page. Its tables are wider than a phone, so they scroll on
   their own rather than pushing the page sideways. */
.admin-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.admin-scroll table { min-width: 520px; }
.admin-stats { row-gap: var(--s-5); }
.admin-of { font-size: 0.5em; font-weight: 400; }
.admin-bad { color: var(--warn); }
.admin-error code { white-space: normal; word-break: break-word; font-size: var(--t-micro); }
.admin-actions { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-4); }
.admin-actions form { margin: 0; }
@media (max-width: 640px) {
  .grid.three.admin-stats { grid-template-columns: 1fr 1fr; }
}

/* The two admin pages, as tabs above the headline. */
.admin-tabs {
  display: flex; gap: var(--s-1); margin: 0 0 var(--s-5);
  border-bottom: 1px solid var(--line);
}
.admin-tabs a {
  padding: var(--s-2) var(--s-3); margin-bottom: -1px;
  color: var(--muted); font-size: var(--t-small); font-weight: 500;
  border-bottom: 2px solid transparent;
}
.admin-tabs a:hover { color: var(--ink); text-decoration: none; }
.admin-tabs a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--accent); }

/* ---------- admin: the database page ----------
   Four kinds of record, one colour each, and the colour follows the record
   everywhere on the page: the strip, the legend, the calendar, the table
   list. `--g` is set by `data-group` and everything below reads it.
   Amber is kept out of the four on purpose - it means "look at this". */
:root {
  --db-people: #4a6fa5;
  --db-fares: #2f9e5b;
  --db-seen: #7c5cc4;
  --db-meta: #b4bec8;
  --db-attention: #e38a1c;
  --db-empty: #e8ecef;
}
[data-group="people"] { --g: var(--db-people); }
[data-group="fares"] { --g: var(--db-fares); }
[data-group="seen"] { --g: var(--db-seen); }
[data-group="meta"] { --g: var(--db-meta); }

.db-head h1 { margin-bottom: var(--s-1); }
.db-head code, .db-notice code { font-size: 0.92em; word-break: break-all; }
/* Something for the operator to act on, so it takes the amber of `.pill.wait`
   rather than the green, which on this site means a fare under the limit. */
.db-notice { background: #f7ecd9; color: var(--warn); }

.db-key {
  display: inline-block; width: 10px; height: 10px; border-radius: 3px;
  background: var(--g, var(--db-meta)); flex: none;
}

/* Where the rows are: one strip, a segment per table. */
.db-manifest-top {
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: var(--s-2) var(--s-5);
}
.db-manifest-top h2 { margin: 0; }
.db-total { margin: 0; color: var(--muted); font-size: var(--t-small); }
.db-total .num {
  font-family: var(--font-num); font-weight: 600; font-size: var(--t-lg);
  color: var(--ink); letter-spacing: -0.02em; margin-right: var(--s-1);
}
.db-strip {
  display: flex; gap: 2px; height: 56px; margin: var(--s-5) 0 var(--s-4);
  border-radius: var(--r-md); overflow: hidden;
}
.db-seg { flex-basis: 0; min-width: 3px; background: var(--g); }
.db-seg:hover { filter: brightness(1.12); }
.db-legend {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s-3) var(--s-5);
}
.db-legend li {
  display: grid; grid-template-columns: auto 1fr; column-gap: var(--s-2);
  align-items: baseline; font-size: var(--t-small);
}
.db-legend .db-key { align-self: start; margin-top: 5px; }
.db-legend .num, .db-legend-share { grid-column: 2; }
.db-legend .num { font-family: var(--font-num); font-weight: 600; font-size: var(--t-h2); line-height: 1.2; }
.db-legend-share { color: var(--muted); font-size: var(--t-micro); }

/* The write calendar: the one loud thing on the page. A row per kind of
   record, a square per day, the same grid the fare calendar is drawn on. */
.db-cal h2 { margin: 0 0 var(--s-1); }
.db-cal-sub { max-width: var(--measure); margin-bottom: var(--s-5); }
.db-cal-grid {
  display: grid;
  grid-template-columns: 11.5rem minmax(0, 1fr) 3.5rem;
  column-gap: var(--s-4); row-gap: var(--s-2);
  align-items: center;
}
.db-cal-months {
  grid-column: 2; display: grid;
  grid-template-columns: repeat(var(--days), minmax(0, 1fr));
  color: var(--muted); font-size: var(--t-micro);
}
.db-cal-months span { white-space: nowrap; }
.db-cal-totalhead { grid-column: 3; text-align: right; color: var(--muted); font-size: var(--t-micro); }
.db-cal-row { display: contents; }
.db-cal-label { grid-column: 1; font-size: var(--t-small); display: flex; align-items: center; gap: var(--s-2); }
.db-cal-label::before {
  content: ""; width: 10px; height: 10px; border-radius: 3px; background: var(--g); flex: none;
}
.db-cal-cells {
  grid-column: 2;
  list-style: none; margin: 0; padding: 0; display: grid;
  grid-template-columns: repeat(var(--days), minmax(0, 1fr)); gap: 3px;
}
.db-cell {
  display: block; aspect-ratio: 1; border-radius: var(--r-sm);
  background: var(--db-empty);
}
.db-cell[data-level="1"] { background: color-mix(in srgb, var(--g) 28%, var(--db-empty)); }
.db-cell[data-level="2"] { background: color-mix(in srgb, var(--g) 52%, var(--db-empty)); }
.db-cell[data-level="3"] { background: color-mix(in srgb, var(--g) 76%, var(--db-empty)); }
.db-cell[data-level="4"] { background: var(--g); }
/* Today is the last column, and a ring says so. */
.db-cal-cells .db-cell:last-child { outline: 1.5px solid var(--ink-soft); outline-offset: 1px; }
.db-cal-total { grid-column: 3; text-align: right; font-family: var(--font-num); font-weight: 600; font-size: var(--t-small); }
.db-cal-foot {
  grid-column: 2; display: flex; justify-content: space-between; align-items: center;
  gap: var(--s-3); margin-top: var(--s-2); color: var(--muted); font-size: var(--t-micro);
}
.db-scale { display: inline-flex; align-items: center; gap: 3px; --g: var(--ink-soft); }
.db-scale .db-cell { width: 11px; }
.db-scale .db-cell:first-of-type { margin-left: var(--s-1); }
.db-scale .db-cell:last-of-type { margin-right: var(--s-1); }

/* One moment of motion: the calendar fills in left to right, once. */
@media (prefers-reduced-motion: no-preference) {
  .db-cal-cells .db-cell {
    animation: db-cell-in 360ms cubic-bezier(.2, .7, .2, 1) both;
    animation-delay: calc(var(--i) * 18ms);
  }
}
@keyframes db-cell-in {
  from { opacity: 0; transform: scale(.4); }
  to { opacity: 1; transform: none; }
}

/* Freshness: two short stacked bars. */
.db-fresh { display: grid; grid-template-columns: 1fr 1fr; gap: 0 20px; }
.db-fresh-card h2 { font-size: var(--t-body); margin: 0 0 var(--s-4); }
.db-fresh-bar {
  display: flex; gap: 2px; height: 14px; border-radius: var(--r-pill); overflow: hidden;
  background: var(--db-empty);
}
.db-fresh-bar span { flex-basis: 0; min-width: 4px; }
[data-tone="fresh"] { --t: var(--db-people); }
[data-tone="today"] { --t: color-mix(in srgb, var(--db-people) 45%, var(--db-empty)); }
[data-tone="stale"] { --t: var(--db-attention); }
.db-fresh-bar [data-tone] { background: var(--t); }
.db-fresh-list { margin: var(--s-4) 0 0; }
.db-fresh-list div {
  display: flex; justify-content: space-between; align-items: center;
  padding: var(--s-1) 0; font-size: var(--t-small);
}
.db-fresh-list dt { display: flex; align-items: center; gap: var(--s-2); }
.db-fresh-list .db-key { background: var(--t); }
.db-fresh-list dd { margin: 0; font-family: var(--font-num); font-weight: 600; }
.db-fresh-hint { margin: calc(-1 * var(--s-3)) 0 var(--s-5); }

/* Every table. */
.db-tables td { vertical-align: middle; }
.db-tables td:first-child { line-height: 1.35; }
.db-name { font-family: var(--font-num); font-size: var(--t-small); margin: 0 var(--s-1) 0 var(--s-2); }
.db-note { display: block; margin-left: 18px; color: var(--muted); font-size: var(--t-micro); }
.db-tables .pill { font-size: var(--t-micro); vertical-align: 1px; }
.db-bar-col { width: 34%; }
.db-bar {
  display: block; height: 8px; min-width: 2px; border-radius: var(--r-pill);
  background: var(--g);
}
.db-tables td.num { font-family: var(--font-num); }

@media (max-width: 720px) {
  .db-legend { grid-template-columns: 1fr 1fr; }
  .db-fresh { grid-template-columns: 1fr; }
  /* The label moves above its row so the thirty days keep the full width. */
  .db-cal-grid { grid-template-columns: minmax(0, 1fr) auto; row-gap: var(--s-1); }
  .db-cal-months, .db-cal-foot { grid-column: 1; }
  .db-cal-totalhead, .db-cal-total { grid-column: 2; }
  .db-cal-label { grid-column: 1 / -1; margin-top: var(--s-3); }
  /* Thirty squares do not fit a phone at any readable size, so the days
     become upright ticks: same order, same shading, a fifth of the width. */
  .db-cal-cells { grid-column: 1; gap: 2px; }
  .db-cal-cells .db-cell { aspect-ratio: auto; height: 22px; border-radius: 2px; }
  .db-bar-col { width: 22%; }
}

/* ---------- admin: the APIs page ----------
   Status is a lamp: blue for working, amber for some errors, red for
   failing, grey for switched off. Official access is a five-step road filled
   in the accent up to where the conversation has got. */
:root {
  --api-ok: #4a6fa5;
  --api-warn: #e38a1c;
  --api-down: #a32f3f;
  --api-off: #b4bec8;
}

.api-board td { vertical-align: middle; }
.api-name { display: block; font-weight: 600; color: var(--ink); }
.api-access {
  display: inline-block; margin-top: 3px; font-size: var(--t-micro); color: var(--muted);
}
.api-access[data-access="licensed"], .api-access[data-access="paid-api"] { color: var(--ink-soft); }
.api-access[data-access="blocked"] { color: var(--api-down); }
.api-sub { display: block; margin-top: 3px; color: var(--muted); font-size: var(--t-micro); }
.api-sub.is-due { color: var(--warn); font-weight: 600; }
.api-next { max-width: 16rem; }

.api-health {
  display: inline-flex; align-items: center; gap: var(--s-2);
  font-weight: 500; white-space: nowrap;
}
.api-health::before {
  content: ""; width: 9px; height: 9px; border-radius: 50%; flex: none;
  background: var(--lamp, var(--api-off));
}
.api-health[data-health="ok"] { --lamp: var(--api-ok); }
.api-health[data-health="degraded"] { --lamp: var(--api-warn); }
.api-health[data-health="down"] { --lamp: var(--api-down); color: var(--api-down); }
.api-health[data-health="off"], .api-health[data-health="idle"] { color: var(--muted); }
/* A failing source is the thing to see first, so its lamp is lit. */
.api-health[data-health="down"]::before { box-shadow: 0 0 0 3px color-mix(in srgb, var(--api-down) 22%, transparent); }

.api-road { display: flex; gap: 3px; width: 96px; margin-bottom: 5px; }
.api-road i { flex: 1; height: 6px; border-radius: 2px; background: var(--db-empty); }
.api-road i.on { background: var(--accent); }
.api-road.is-off i { background: repeating-linear-gradient(
  -45deg, var(--db-empty) 0 3px, transparent 3px 6px); }
.api-stage { font-size: var(--t-small); }
.api-stage[data-stage="approved"] { color: var(--accent); font-weight: 600; }
.api-stage[data-stage="declined"] { color: var(--api-down); }
.api-stage[data-stage="not_started"], .api-stage[data-stage="paused"] { color: var(--muted); }

/* One source, folded. The summary is the board row again, so a folded list
   still reads; opening it is where the numbers and the log are. */
.api-card { padding: 0; }
.api-card > summary {
  display: grid; grid-template-columns: minmax(8rem, 1.2fr) 1fr 1.4fr auto;
  align-items: center; gap: var(--s-3) var(--s-5);
  padding: 18px 22px 18px 44px; cursor: pointer; list-style: none; position: relative;
}
.api-card > summary::-webkit-details-marker { display: none; }
.api-card > summary::before {
  content: ""; position: absolute; left: 22px; top: 50%;
  width: 7px; height: 7px; margin-top: -5px;
  border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted);
  transform: rotate(-45deg); transition: transform .15s;
}
.api-card[open] > summary::before { transform: rotate(45deg); margin-top: -6px; }
.api-card > summary:hover .api-card-name { color: var(--accent); }
.api-card-name { font-weight: 600; font-size: var(--t-body); }
.api-card-stage { display: flex; align-items: center; gap: var(--s-3); }
.api-card-stage .api-road { margin: 0; }
.api-card-log { color: var(--muted); font-size: var(--t-micro); text-align: right; }
.api-card[open] > summary { border-bottom: 1px solid var(--line-soft); }
@media (prefers-reduced-motion: reduce) {
  .api-card > summary::before { transition: none; }
}

.api-card h3 { font-size: var(--t-small); font-weight: 600; margin: 0 0 var(--s-3); }
.api-card-body {
  display: grid; grid-template-columns: 1fr 1.25fr; gap: var(--s-6);
  padding: 22px; border-bottom: 1px solid var(--line-soft);
}
.api-facts dl { margin: 0; }
.api-facts dl div {
  display: flex; justify-content: space-between; gap: var(--s-3);
  padding: 6px 0; border-bottom: 1px solid var(--line-soft); font-size: var(--t-small);
}
.api-facts dt { color: var(--ink-soft); }
.api-facts dd { margin: 0; font-family: var(--font-num); font-weight: 600; text-align: right; }
.api-facts dd .muted { font-weight: 400; }
.api-error { margin: var(--s-4) 0 0; font-size: var(--t-micro); }
.api-error .muted { display: block; margin-bottom: var(--s-1); }
.api-error code { display: block; white-space: normal; word-break: break-word; color: var(--ink-soft); }

.api-form { display: grid; gap: var(--s-4); }
.api-form-pair { display: grid; grid-template-columns: 1fr 10.5rem; gap: var(--s-3); }
.api-form-foot { display: flex; align-items: center; gap: var(--s-4); }
.api-form-foot .api-sub { margin: 0; }
.api-card input[type=date], .api-card textarea {
  width: 100%; padding: 10px 12px; font: inherit; color: var(--ink);
  background: var(--field); border: 1px solid var(--line); border-radius: var(--r-md);
  transition: border-color .12s, box-shadow .12s;
}
.api-card textarea { resize: vertical; min-height: 44px; line-height: 1.45; }
.api-card input[type=date]:focus, .api-card textarea:focus {
  border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft);
}

.api-log { padding: 22px; }
.api-note-form {
  display: grid; grid-template-columns: 10.5rem 11rem 1fr auto;
  gap: var(--s-3); align-items: end; margin-bottom: var(--s-5);
}
.api-note-form button { height: 44px; }

/* The log, newest first, as a line with a mark per exchange. */
.api-timeline { list-style: none; margin: 0; padding: 0; }
.api-timeline li {
  position: relative; padding: 0 0 var(--s-4) 26px;
}
.api-timeline li::before {
  content: ""; position: absolute; left: 4px; top: 16px; bottom: 0;
  width: 2px; background: var(--line-soft);
}
.api-timeline li:last-child::before { display: none; }
.api-timeline li::after {
  content: ""; position: absolute; left: 0; top: 5px;
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--panel); border: 2px solid var(--mark, var(--line-strong));
}
.api-timeline li[data-kind="sent"] { --mark: var(--api-ok); }
.api-timeline li[data-kind="received"] { --mark: var(--accent); }
.api-timeline li[data-kind="received"]::after { background: var(--accent); }
.api-timeline li[data-kind="call"] { --mark: var(--db-seen); }
.api-timeline li[data-kind="applied"] { --mark: var(--api-warn); }
.api-timeline-head { display: flex; align-items: baseline; gap: var(--s-3); flex-wrap: wrap; }
.api-timeline time { font-family: var(--font-num); font-size: var(--t-micro); color: var(--muted); }
.api-kind { font-size: var(--t-small); font-weight: 600; }
.api-timeline-head form { margin: 0 0 0 auto; }
.api-remove {
  padding: 2px 6px; background: none; border: none; color: var(--muted);
  font-size: var(--t-micro); font-weight: 400; cursor: pointer;
}
.api-remove:hover { background: none; color: var(--danger); text-decoration: underline; }
.api-timeline p {
  margin: var(--s-1) 0 0; font-size: var(--t-small); color: var(--ink-soft);
  white-space: pre-line; max-width: var(--measure);
}

@media (max-width: 720px) {
  .api-card > summary { grid-template-columns: 1fr auto; padding-left: 40px; }
  .api-card-stage { grid-column: 1 / -1; }
  .api-card-log { display: none; }
  .api-card-body { grid-template-columns: 1fr; gap: var(--s-5); padding: 18px; }
  .api-log { padding: 18px; }
  .api-note-form { grid-template-columns: 1fr 1fr; }
  .api-note-body, .api-note-form button { grid-column: 1 / -1; }
  .api-form-pair { grid-template-columns: 1fr; }
}
