/* ==========================================================================
   #FONTS
   ==========================================================================
   Plus Jakarta Sans, self-hosted. One variable file per subset covers every
   weight the stylesheet asks for (400/500/600/700) in a single request, where
   the four static instances would have been four.

   Self-hosted rather than loaded from Google: a stylesheet from another origin
   is a render-blocking request that has to resolve DNS, open a TLS connection
   and be parsed before the browser even learns which font files it needs. From
   here the @font-face is in a stylesheet the page is already fetching, and the
   file itself is preloaded in the head.

   Licensed under the SIL Open Font License 1.1, which permits this -- see
   fonts/OFL.txt, shipped alongside as the licence requires.

   Paths are relative to this file, so they follow the stylesheet if it is ever
   served from the CDN rather than the app. They must also match the preload in
   the page head exactly, cache-buster included, or the file is fetched twice.

   Subsets are split on unicode-range: a page renders only latin unless it puts
   a character on screen from one of the others, so the cyrillic and vietnamese
   files cost nothing until something needs them. The data needs latin and one
   latin-ext character today (the S-caron in Ljubljana's airport name); the rest
   is there so a name added later never falls back mid-word. */

@font-face {
  font-family: "Plus Jakarta Sans";
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url("../fonts/plus-jakarta-sans-latin.woff2") 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: "Plus Jakarta Sans";
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url("../fonts/plus-jakarta-sans-latin-ext.woff2") 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: "Plus Jakarta Sans";
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url("../fonts/plus-jakarta-sans-vietnamese.woff2") format("woff2");
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}

@font-face {
  font-family: "Plus Jakarta Sans";
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url("../fonts/plus-jakarta-sans-cyrillic-ext.woff2") format("woff2");
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}

/* ==========================================================================
   #PALETTE
   ==========================================================================
   This app used to ship Bootstrap's stock palette untouched -- #0d6efd and
   friends -- which is the single most recognisable "built on Bootstrap" tell
   there is. Two colours replace it:

     --brand-ink     deep navy. Chrome, structure, headings, data labels.
     --brand-accent  deep teal. Reserved for the ONE action per screen, for
                     "you are here" state, and for whatever the pointer is
                     currently on.

   The split is the point. Sixteen elements on the results page were the same
   saturated blue, so nothing led. Ink carries the furniture; accent is spent
   sparingly and therefore still means something.

   Teal replaced a burnt orange, and not only on taste. The caution tier of
   the notice chips is amber (#8A4B00): with an orange accent, the colour that
   means "act here" and the colour that means "check this" were the same hue
   family, side by side on every result card. Teal reads clear of both the
   amber and the red, so the three notice tiers stay legible as tiers.

   There used to be a third token, --brand-flag, a brighter orange for the
   hovered route segment. It was declared and never referenced -- the route
   rules kept their literal, one of which coloured text at 3.15:1. Hover now
   takes the accent, which is both one rule fewer and 5.47:1.

   Contrast is measured, not assumed. Every pairing clears WCAG AA:
     white on ink 15.60   white on accent 5.47   accent on white 5.47
     white on ink-700 11.46   white on accent-700 8.06
     accent on #f8f9fa 5.19 (the outline buttons)   accent on tint 4.78
   And as a graphic against its neighbours on the route bar, the accent is
   3.66 on a leg and 4.20 on a stop -- past the 3:1 a non-text mark needs. */
:root {
  --brand-ink:        #10243F;
  --brand-ink-700:    #1B3A63;

  /* The same navy, named twice, because in a dark palette it cannot be one
     colour. Measured: for `--brand-ink` to clear AA as text on a dark panel it
     has to lift to about #5B7FA8, and white on #5B7FA8 is 4.16 -- under the
     bar. No navy clears both, because the two roles pull opposite ways. In the
     light palette they never had to: text and fill both wanted "dark", which
     is why one token served until now.

     `--brand-ink` is the text and the outline. `--brand-ink-fill` is the band
     behind white glyphs -- the hero bar, the footer, a filled chip -- and in a
     dark palette it stays deep while the text lightens away from it. */
  --brand-ink-fill:     var(--brand-ink);
  --brand-ink-fill-700: var(--brand-ink-700);

  /* And one more division inside the fill, which only a dark palette forces.
     `--brand-ink-fill` is an expanse -- `html` itself, the hero, the search
     band, the footer -- and wants to be the darkest thing on the page. The
     chips wearing the same navy today (a primary badge, the current sort tab,
     the current directory letter, the ink button) sit *on* a panel and have
     to be seen against it. Measured: a navy dark enough to be the page is 1.09
     against the panel, which is no edge at all. The chip lifts to #4F6D9C in
     the dark block -- 3.25 on the panel, 5.25 for the white glyph on it. */
  --brand-ink-chip:     var(--brand-ink);
  --brand-ink-chip-700: var(--brand-ink-700);

  /* The same split, for every other colour that is worn both ways. Measured
     against a #141C2B panel: teal #14988C reads at 4.79 and carries white at
     3.56; green #1E9E5E reads at 4.96 and carries 3.44; red #D1434F reads at
     3.76 and carries 4.54. None clears AA both ways, so none can stay one
     token once there is a dark palette to serve.

     In this palette every `-fill` is its text twin, so nothing below changes
     until the dark block redefines them apart. */
  --brand-accent-fill:      var(--brand-accent);
  --brand-accent-fill-700:  var(--brand-accent-700);
  --brand-success-fill:     var(--brand-success);
  --brand-danger-fill:      var(--brand-danger);
  --brand-accent:     #0F766E;
  --brand-accent-700: #0B5A54;
  --brand-tint:       #E6F2F1;
  --brand-tint-edge:  #BFDDDA;

  /* Semantics. Bootstrap's stock success and danger both clear AA on pure
     white by a hair -- 4.53 -- and fail the moment they sit on a tinted
     surface: 4.30 against the #f8f9fa card footers this app uses everywhere.
     Both are darkened one step, which also sits better beside the navy than
     the brighter originals did. */
  --brand-success:    #146C43;   /* 6.45 on white, 6.12 on #f8f9fa */
  --brand-danger:     #B02A37;   /* 6.50 on white, 6.16 on #f8f9fa */
  --brand-danger-700: #96232E;   /* pressed and hovered, as accent-700 is */

  /* Tints for the two semantics, mirroring --brand-tint / --brand-tint-edge.
     These are not new colours: every value here was already in the file. What
     was missing was a name, and without one the same job had been mixed by
     hand each time it came up -- six near-identical pale pinks across the
     checkout alert, the notice chips, a cancelled booking and a danger hover,
     and two pale greens. The ink that goes on them measures 5.56 and 5.59. */
  /* Categorical, and decorative only.
     Colours whose job is to tell one section from another. Neither brand hue
     can do that alone, and the semantics must not be borrowed for it -- green
     as a category would collide with green as "good".
     Bright on purpose. The first attempt held the brand teal's own lightness
     and chroma and rotated hue, which produced a well-behaved family and a
     drab one: at that chroma an orange comes out salmon. Holding one lightness
     across four hues is what forces that, so each of these takes the lightness
     its own hue needs, the way the reference set does -- its yellow is light
     and its red is dark.
     White glyph on each, measured: 3.45, 5.02, 3.58, 5.55. All clear the 3:1 a
     graphic needs; blue and violet also clear 4.5. Amber is the one this family
     cannot offer -- #F08C00 measures 2.48 and no white mark belongs on it.
     Nothing wearing these carries state, so none has to mean anything. */
  --cat-green:         #2F9E44;
  --cat-blue:          #1971C2;
  --cat-orange:        #E8590C;
  --cat-violet:        #7048E8;
  --cat-pink:          #C2255C;

  --success-tint:      #E4F2EA;
  --success-tint-edge: #B2E2C4;
  --danger-tint:       #FBE9EB;
  --danger-tint-edge:  #F3C7CC;

  /* Muted text. This file used Bootstrap's gray-500 in 25 places -- 2.07:1 on
     white, which is not only past AA but past the 3:1 floor for a graphic, and
     it was carrying real content: layover breakdowns, the CAD suffix on every
     price, field notes. Gray-600 is not the fix either; it measures 4.45 on
     the #f8f9fa card footers and misses by 0.05.
     Fixing the faint tier inverted the scale: what had been the lightest text
     ended up darker than the "secondary" tier above it, so gray-600 had to
     move down as well. Four steps now, each perceptibly apart and every one
     of them compliant (white / #f8f9fa):
       --bs-body-color   #212529   body                    15.43 / 14.68
       --ink-secondary   #495057   secondary text           8.18 / 7.76
       --ink-muted       #646D75   quiet detail             5.27 / 5.00
       --ink-faint       #8A9199   ornament only, 3:1 bar   3.19 / 3.02
     Only two things use faint: the dashed booking-reference border and the
     2.5rem icon on the expired-checkout page. */
  --ink-body:         #151A21;   /* 17.0 on white, 15.3 on --surface */
  --ink-secondary:    #3F4854;   /*  9.3 on white,  8.3 on --surface */
  --ink-muted:        #5F6875;   /*  5.6 on white,  5.0 on --surface */
  --ink-faint:        #828C99;   /*  3.5 on white -- ornament only */

  /* Surfaces and rules, named rather than scattered as Bootstrap greys. They
     are cooled a few degrees toward blue: the whole neutral field reads
     colder, which is most of what makes an airline search feel like one
     rather than like a generic dashboard. Every one of these was a literal
     repeated a dozen times before. */
  /* White, and the two jobs it was doing under one spelling.

     83 of this file's 145 untokenised colours were `#fff`, and they split
     cleanly by property: `background-color` meant a panel lifted above the
     page, and `color` meant a glyph sitting on a filled brand colour. Only
     the first is a surface, and only the first may move when the palette
     does -- a dark theme that flipped both would print white text on white
     buttons.

     `--surface` is the ground these sit on, which is why it is not white. */
  --surface-raised:   #FFFFFF;   /* panels lifted above the page */
  --on-brand:         #FFFFFF;   /* glyph on a filled brand colour, never flips */

  /* The knob inside a switch that is off. Bootstrap ships one at 25% black and
     swaps it for 25% white under `data-bs-theme`, and both are too faint to
     find on a track that is nearly the panel colour -- which is what made the
     filter switches look washed out. A solid grey reads at either end. */
  --switch-knob: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23868E96'/%3e%3c/svg%3e");
  --surface:          #EFF2F7;   /* raised panels, card footers */
  --surface-sunken:   #E8EDF3;   /* wells, quiet chips */
  --line:             #E1E6ED;   /* default rule */
  --line-strong:      #D3DAE3;   /* emphasis rule, route track */
  --line-strongest:   #C2CBD6;   /* filled route leg */

  /* The same scale does not run backwards. #adb5bd was removed above for
     measuring 2.07 on white; on the navy footer it measures 7.52 and is
     exactly right. A colour is not accessible or inaccessible -- a pairing
     is. These two are for dark surfaces only. */
  --ink-on-dark:      #ADB5BD;   /* 7.52 on --brand-ink  -- quiet prose */
  --ink-on-dark-lead: #DEE2E6;   /* 11.98 on --brand-ink -- figures, emphasis */
  /* The rule to match them. --line and its neighbours are mixed for white and
     come out as glare here; a rule on a dark surface is the surface lightened,
     not a light colour laid on top, so this is white at low alpha and picks up
     whatever it sits on. */
  --line-on-dark:     rgba(255, 255, 255, .12);
  /* Success and danger for a dark surface. --brand-success and --brand-danger
     are mixed for white and measure 1.3 and 1.7 on the navy card -- unreadable
     -- so the pair is lightened rather than reused. Both against
     --brand-ink-700, which is the only dark surface either appears on. */
  --good-on-dark:     #6EE7B7;   /* 7.52 on --brand-ink-700 */
  --bad-on-dark:      #FCA5A5;   /* 6.04 on --brand-ink-700 */

  /* Bootstrap's own tokens, remapped. Structural blue becomes ink. */
  --bs-body-color: var(--ink-body);
  --bs-primary: var(--brand-ink);
  /* The header is `.bg-primary`, and `.bg-primary` paints from this triplet
     rather than from the variable above it -- which is why it went on wearing
     the light navy after the hero and the footer beside it went dark, leaving
     a seam across the top of every page. It is the band colour and not the ink,
     so the dark block pins it to `--brand-ink-fill` rather than letting it
     follow `--brand-ink`, which is text and lifts. */
  --bs-primary-rgb: 16, 36, 63;
  --bs-primary-bg-subtle: var(--brand-tint);
  --bs-primary-border-subtle: var(--brand-tint-edge);
  --bs-primary-text-emphasis: var(--brand-accent-700);
  --bs-link-color: var(--brand-accent);
  --bs-link-color-rgb: 15, 118, 110;
  --bs-link-hover-color: var(--brand-accent-700);
  /* Bootstrap defaults to 6px. 8px on controls and 12px on cards is the
     softer geometry, and the file already reached for .75rem and 1rem by
     hand in places, so this only makes that consistent. */
  --bs-border-radius: .5rem;
  --bs-border-radius-lg: .75rem;
  --bs-card-border-radius: .75rem;   /* see the .card override below */
  --bs-success: var(--brand-success);
  --bs-success-rgb: 20, 108, 67;
  --bs-danger: var(--brand-danger);
  --bs-danger-rgb: 176, 42, 55;

  /* The dark palette, defined here and assigned twice below. Spelled once
     so a value is changed in one place; the two assignment blocks cannot be
     one rule, because a media query and an attribute selector do not combine. */
  --d-surface-raised:      #141C2B;   /* panels lift toward the light, not away */
  --d-surface:             #0B111C;   /* the ground they sit on */
  --d-surface-sunken:      #070C14;   /* and the well below it */
  --d-line:                #232C3D;
  --d-line-strong:         #2F3A4E;
  --d-line-strongest:      #3D4A61;
  --d-ink-body:            #E4E8EF;   /* 13.88 on a panel -- not pure white, which haloes */
  --d-ink-secondary:       #B4BDCB;   /* 9.00 */
  --d-ink-muted:           #8D97A7;   /* 5.78 */
  --d-ink-faint:           #6A7484;   /* 3.61, the 3:1 ornament bar */
  --d-brand-ink:           #7FA3C4;   /* 6.45 -- the text half of the navy */
  --d-brand-ink-700:       #9CBADA;   /* 8.49 */
  /* The band, and it is lighter than the page rather than darker.

     First attempt had this at #0A101C, which is 1.01 against the ground -- so
     the header, the search band and the footer all dissolved into the page and
     the site lost its bars. In the light palette this is a dark navy on a light
     page and reads as a bar; the dark palette has to earn the same separation
     from the other side, by lifting off the ground instead of sinking into it. */
  --d-brand-ink-fill:      #1C2942;   /* 1.30 against the ground, 14.53 for white */
  --d-brand-ink-fill-700:  #27375A;   /* a block lifted off that band */
  --d-brand-ink-chip:      #4F6D9C;   /* 3.25 on a panel, 5.25 for the glyph */
  --d-brand-ink-chip-700:  #5D7FB0;

  /* The fills, pinned. In `:root` each of these is `var(--brand-accent)` and
     friends, so redefining the text token above dragged the fills with it --
     which is how every accent button turned into pale teal carrying white at
     2.20. They are saturated mid-tones on purpose: dark enough for a white
     glyph, and they were already the right darkness, so most keep their light
     value. Success and danger lift a step to clear 3:1 as blocks on a panel
     without dropping their label below 4.5. */
  --d-brand-accent-fill:      #0F766E;   /* 3.12 on a panel, 5.47 for white */
  --d-brand-accent-fill-700:  #0B5A54;   /* the hover, deeper on purpose */
  --d-brand-success-fill:     #17804F;   /* 3.44 / 4.96 */
  --d-brand-danger-fill:      #C93A47;   /* 3.40 / 5.02 */
  --d-switch-knob: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23CBD2DA'/%3e%3c/svg%3e");
  --d-brand-accent:        #2AC3B2;   /* 7.76 */
  --d-brand-accent-700:    #5EEAD4;   /* 11.53 */
  --d-brand-success:       #3DD68C;   /* 9.10 */
  --d-brand-danger:        #F1707C;   /* 5.97 */
  --d-brand-tint:          #0E2A28;   /* the pale teal surface, inverted */
  --d-brand-tint-edge:     #1B4340;
  --d-success-tint:        #0E2A1B;
  --d-success-tint-edge:   #1C4A31;
  --d-danger-tint:         #2A1216;
  --d-danger-tint-edge:    #4A2027;
}

/* Dark, two ways in.

   The media query is the one that works with no JavaScript, and it yields to
   an explicit choice: `:not([data-theme="light"])` means a reader who asked
   for light keeps it on a machine set to dark. The attribute rule is what the
   switcher writes, and it comes second so it wins in both directions.

   Only tokens are redefined. Nothing below this point in the file knows there
   are two palettes, which is what the four preparation commits bought. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --surface-raised: var(--d-surface-raised);
    --surface: var(--d-surface);
    --surface-sunken: var(--d-surface-sunken);
    --line: var(--d-line);
    --line-strong: var(--d-line-strong);
    --line-strongest: var(--d-line-strongest);
    --ink-body: var(--d-ink-body);
    --ink-secondary: var(--d-ink-secondary);
    --ink-muted: var(--d-ink-muted);
    --ink-faint: var(--d-ink-faint);
    --brand-ink: var(--d-brand-ink);
    --brand-ink-700: var(--d-brand-ink-700);
    --brand-ink-fill: var(--d-brand-ink-fill);
    --brand-ink-fill-700: var(--d-brand-ink-fill-700);
    --brand-accent-fill: var(--d-brand-accent-fill);
    --brand-accent-fill-700: var(--d-brand-accent-fill-700);
    --brand-success-fill: var(--d-brand-success-fill);
    --brand-danger-fill: var(--d-brand-danger-fill);
    --switch-knob: var(--d-switch-knob);
    --bs-primary-rgb: 28, 41, 66;
    /* The other triplets, for the same reason as the one above: Bootstrap's
       utilities paint from these and not from the variables beside them, so a
       literal here keeps its light value while everything around it moves.
       `.text-success` was #146C43 on a dark panel at 2.65 because of it. */
    --bs-success-rgb: 61, 214, 140;
    --bs-danger-rgb: 241, 112, 124;
    --bs-link-color-rgb: 42, 195, 178;
    --brand-ink-chip: var(--d-brand-ink-chip);
    --brand-ink-chip-700: var(--d-brand-ink-chip-700);
    --brand-accent: var(--d-brand-accent);
    --brand-accent-700: var(--d-brand-accent-700);
    --brand-success: var(--d-brand-success);
    --brand-danger: var(--d-brand-danger);
    --brand-tint: var(--d-brand-tint);
    --brand-tint-edge: var(--d-brand-tint-edge);
    --success-tint: var(--d-success-tint);
    --success-tint-edge: var(--d-success-tint-edge);
    --danger-tint: var(--d-danger-tint);
    --danger-tint-edge: var(--d-danger-tint-edge);

    /* Bootstrap's own palette, which is a second set of names for the same
       surfaces and was still light after everything above went dark. It paints
       `.list-group-item`, `.card`, `.table`, `.form-control` and the dropdowns
       -- which is why the top-searches list, the saved flights, the results and
       the filter rail all stayed white while the page around them changed.

       Set here and not only by `data-bs-theme` on the element, because that
       attribute is written by JavaScript: a reader whose machine is set to dark
       and who has JavaScript off would otherwise get this file's dark palette
       with Bootstrap's light one inside it. The attribute is still set as well,
       for the components that read variables this list does not name. */
    --bs-body-bg: var(--d-surface-raised);
    --bs-body-bg-rgb: 20, 28, 43;
    --bs-secondary-bg: var(--d-surface-sunken);
    --bs-secondary-bg-rgb: 7, 12, 20;
    --bs-tertiary-bg: var(--d-surface-sunken);
    --bs-tertiary-bg-rgb: 7, 12, 20;
    --bs-secondary-color: var(--d-ink-muted);
    --bs-tertiary-color: var(--d-ink-faint);
    --bs-emphasis-color: var(--d-ink-body);
    --bs-emphasis-color-rgb: 228, 232, 239;
    --bs-heading-color: var(--d-ink-body);
    --bs-border-color: var(--d-line);
    --bs-border-color-translucent: rgba(255, 255, 255, .12);
    --bs-light: var(--d-surface-sunken);
    --bs-light-rgb: 7, 12, 20;
  }
}

:root[data-theme="dark"] {
    --surface-raised: var(--d-surface-raised);
    --surface: var(--d-surface);
    --surface-sunken: var(--d-surface-sunken);
    --line: var(--d-line);
    --line-strong: var(--d-line-strong);
    --line-strongest: var(--d-line-strongest);
    --ink-body: var(--d-ink-body);
    --ink-secondary: var(--d-ink-secondary);
    --ink-muted: var(--d-ink-muted);
    --ink-faint: var(--d-ink-faint);
    --brand-ink: var(--d-brand-ink);
    --brand-ink-700: var(--d-brand-ink-700);
    --brand-ink-fill: var(--d-brand-ink-fill);
    --brand-ink-fill-700: var(--d-brand-ink-fill-700);
    --brand-accent-fill: var(--d-brand-accent-fill);
    --brand-accent-fill-700: var(--d-brand-accent-fill-700);
    --brand-success-fill: var(--d-brand-success-fill);
    --brand-danger-fill: var(--d-brand-danger-fill);
    --switch-knob: var(--d-switch-knob);
    --bs-primary-rgb: 28, 41, 66;
    /* The other triplets, for the same reason as the one above: Bootstrap's
       utilities paint from these and not from the variables beside them, so a
       literal here keeps its light value while everything around it moves.
       `.text-success` was #146C43 on a dark panel at 2.65 because of it. */
    --bs-success-rgb: 61, 214, 140;
    --bs-danger-rgb: 241, 112, 124;
    --bs-link-color-rgb: 42, 195, 178;
    --brand-ink-chip: var(--d-brand-ink-chip);
    --brand-ink-chip-700: var(--d-brand-ink-chip-700);
    --brand-accent: var(--d-brand-accent);
    --brand-accent-700: var(--d-brand-accent-700);
    --brand-success: var(--d-brand-success);
    --brand-danger: var(--d-brand-danger);
    --brand-tint: var(--d-brand-tint);
    --brand-tint-edge: var(--d-brand-tint-edge);
    --success-tint: var(--d-success-tint);
    --success-tint-edge: var(--d-success-tint-edge);
    --danger-tint: var(--d-danger-tint);
    --danger-tint-edge: var(--d-danger-tint-edge);

    /* Bootstrap's own palette, which is a second set of names for the same
       surfaces and was still light after everything above went dark. It paints
       `.list-group-item`, `.card`, `.table`, `.form-control` and the dropdowns
       -- which is why the top-searches list, the saved flights, the results and
       the filter rail all stayed white while the page around them changed.

       Set here and not only by `data-bs-theme` on the element, because that
       attribute is written by JavaScript: a reader whose machine is set to dark
       and who has JavaScript off would otherwise get this file's dark palette
       with Bootstrap's light one inside it. The attribute is still set as well,
       for the components that read variables this list does not name. */
    --bs-body-bg: var(--d-surface-raised);
    --bs-body-bg-rgb: 20, 28, 43;
    --bs-secondary-bg: var(--d-surface-sunken);
    --bs-secondary-bg-rgb: 7, 12, 20;
    --bs-tertiary-bg: var(--d-surface-sunken);
    --bs-tertiary-bg-rgb: 7, 12, 20;
    --bs-secondary-color: var(--d-ink-muted);
    --bs-tertiary-color: var(--d-ink-faint);
    --bs-emphasis-color: var(--d-ink-body);
    --bs-emphasis-color-rgb: 228, 232, 239;
    --bs-heading-color: var(--d-ink-body);
    --bs-border-color: var(--d-line);
    --bs-border-color-translucent: rgba(255, 255, 255, .12);
    --bs-light: var(--d-surface-sunken);
    --bs-light-rgb: 7, 12, 20;
}

/* Bootstrap declares --bs-card-border-radius on .card itself, so setting it in
   :root above never reaches a card -- the same shape of trap as the button
   colours below. */
.card { --bs-card-border-radius: .75rem; }

/* Bootstrap 5.3 writes the button colours as literal hex into each variant's
   own custom properties, so overriding --bs-primary above does not reach a
   single button. These have to be restated. */
/* Same trap as .btn-primary above: the variant writes its own literal hex, so
   the --bs-danger remap in :root never reaches a button. Needed because the
   cancel dialog dresses SweetAlert's buttons in this app's own. */
.btn-danger {
  --bs-btn-bg: var(--brand-danger-fill);
  --bs-btn-border-color: var(--brand-danger-fill);
  --bs-btn-hover-bg: var(--brand-danger-fill);
  --bs-btn-hover-border-color: var(--brand-danger-fill);
  --bs-btn-active-bg: var(--brand-danger-fill);
  --bs-btn-active-border-color: var(--brand-danger-fill);
}

.btn-primary {
  --bs-btn-bg: var(--brand-accent-fill);
  --bs-btn-border-color: var(--brand-accent-fill);
  --bs-btn-hover-bg: var(--brand-accent-fill-700);
  --bs-btn-hover-border-color: var(--brand-accent-fill-700);
  --bs-btn-active-bg: var(--brand-accent-fill-700);
  --bs-btn-active-border-color: var(--brand-accent-fill-700);
  --bs-btn-disabled-bg: var(--brand-accent-fill);
  --bs-btn-disabled-border-color: var(--brand-accent-fill);
}

.btn-outline-primary {
  --bs-btn-color: var(--brand-accent);
  --bs-btn-border-color: var(--brand-accent);
  --bs-btn-hover-bg: var(--brand-accent-fill);
  --bs-btn-hover-border-color: var(--brand-accent-fill);
  --bs-btn-active-bg: var(--brand-accent-fill);
  --bs-btn-active-border-color: var(--brand-accent-fill);
}

/* Two families of action, so the colour carries meaning instead of just
   shouting. Accent moves you forward through the booking -- choose a flight,
   continue, pay. Ink adjusts what you are looking at -- search again, apply
   filters, change sort. Before this, every one of them was the same fill, and
   the results page had fifteen of them competing. */
.btn-ink {
  --bs-btn-color: var(--on-brand);
  --bs-btn-bg: var(--brand-ink-chip);
  --bs-btn-border-color: var(--brand-ink-chip);
  --bs-btn-hover-color: var(--on-brand);
  --bs-btn-hover-bg: var(--brand-ink-chip-700);
  --bs-btn-hover-border-color: var(--brand-ink-chip-700);
  --bs-btn-active-color: var(--on-brand);
  --bs-btn-active-bg: var(--brand-ink-chip-700);
  --bs-btn-active-border-color: var(--brand-ink-chip-700);
  --bs-btn-focus-shadow-rgb: 16, 36, 63;
}

.btn-outline-success {
  --bs-btn-color: var(--brand-success);
  --bs-btn-border-color: var(--brand-success);
  --bs-btn-hover-bg: var(--brand-success-fill);
  --bs-btn-hover-border-color: var(--brand-success-fill);
  --bs-btn-active-bg: var(--brand-success-fill);
  --bs-btn-active-border-color: var(--brand-success-fill);
}

/* The one place both names are needed at once: at rest this is ink on nothing,
   and hovered it is a fill with a white glyph on it. */
.btn-outline-ink {
  --bs-btn-color: var(--brand-ink);
  --bs-btn-border-color: var(--brand-ink);
  --bs-btn-hover-color: var(--on-brand);
  --bs-btn-hover-bg: var(--brand-ink-chip);
  --bs-btn-hover-border-color: var(--brand-ink-chip);
  --bs-btn-active-color: var(--on-brand);
  --bs-btn-active-bg: var(--brand-ink-chip);
  --bs-btn-active-border-color: var(--brand-ink-chip);
  --bs-btn-focus-shadow-rgb: 16, 36, 63;
}

/* Same trap as the buttons: the pill's active background is a literal, so the
   trip-type tabs above the search form kept their Bootstrap blue. */
.nav-pills {
  --bs-nav-pills-link-active-bg: var(--brand-accent-fill);
}

/* ion.rangeSlider arrives from a CDN with its own blue (#006cfa) baked into
   the irs--round skin, so the layover and price sliders knew nothing about
   any of this. main.css loads after it, which is what makes these win. */
.irs--round .irs-bar,
.irs--round .irs-from,
.irs--round .irs-to,
.irs--round .irs-single {
  background-color: var(--brand-accent-fill);
}

.irs--round .irs-handle {
  border-color: var(--brand-accent);
}

.irs--round .irs-from:before,
.irs--round .irs-to:before,
.irs--round .irs-single:before {
  border-top-color: var(--brand-accent);
}

/* ==========================================================================
   #MOTION
   ==========================================================================
   Every duration and curve in this file used to be a literal, repeated at
   each site: changing the hover speed meant editing a dozen declarations
   across fifteen hundred lines. Two durations cover almost everything, and
   the two curves are stronger than the CSS keywords, which are too gentle to
   read as deliberate.

   Which curve is a decision, not a default:
     - arriving or leaving  -> --ease-out   (fast first, so it feels answered)
     - moving on screen     -> --ease-in-out (only the card flip earns it)
     - hover and colour     -> plain `ease` is right; it is not movement
     - constant motion      -> linear (the spinner, and only the spinner) */
:root {
  /* Row icons for the place dropdown, as masks so they inherit their colour.
     Inline here rather than as <i> elements: see .combo__icon. */
  --icon-plane: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23000'%3E%3Cpath d='M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5z'/%3E%3C/svg%3E");
  --icon-pin: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23000'%3E%3Cpath d='M12 2a7 7 0 0 0-7 7c0 5.25 7 13 7 13s7-7.75 7-13a7 7 0 0 0-7-7zm0 9.5A2.5 2.5 0 1 1 12 6.5a2.5 2.5 0 0 1 0 5z'/%3E%3C/svg%3E");
  --icon-clock: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23000'%3E%3Cpath d='M13 3a9 9 0 1 0 8.94 10h-2.02A7 7 0 1 1 13 5v4l5-4.5L13 0z'/%3E%3Cpath d='M12.5 8v5l4 2 .75-1.23-3.25-1.77V8z'/%3E%3C/svg%3E");

  /* The column a page gives its content: what .place__blocks measures inside
     the 1140px container once the 17rem rail and the gap are taken out. An
     article has no rail, so it centres this width instead -- and the trail
     above it is centred to match, which is the only reason this is a token
     rather than a number in one place. */
  --content-column: 820px;

  --dur-fast: .12s;   /* hover feedback, press, small reveals */
  --dur-base: .2s;    /* chevrons and anything that rotates or slides */
  --dur-slow: .5s;    /* the card flip -- deliberately unhurried */

  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
}

/* Asking for reduced motion is not asking for no feedback. Colour, shadow,
   opacity and filter all still carry meaning and none of them is vestibular,
   so they keep their transitions; what goes is travel, so a transform lands
   at its destination instead of moving there. Overriding the property list
   rather than the duration is what buys that distinction -- zeroing every
   duration would take the comprehension along with the movement.
   The one looping animation in this file is stopped where it is defined. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-property: color, background-color, border-color, box-shadow, opacity, filter !important;
  }
}

/* ==========================================================================
   #FOOTER COLOUR
   ==========================================================================
   The footer carried Bootstrap's .text-light-emphasis on .bg-dark. That token
   is #495057 and is meant for light surfaces; against #212529 the whole info
   column measured 1.89:1 -- present, but not readable. The utility sets its
   colour with !important, so it had to come off the element rather than be
   overridden here. Figures sit one step brighter than the prose around them,
   which is what the monospace was reaching for with a pink that failed at
   3.43. */
.footer { color: var(--ink-on-dark); }

/* The `code` and `.font-monospace` rules that sat here went with the markup
   they were written for. The render time and the commit were set in monospace
   in an info column, which made the two loudest things in the footer a number
   nobody came for and a hash; both are now one quiet line under the legal text
   -- see .footer__stats. */

/* ==========================================================================
   #FLOATING LABELS
   ==========================================================================
   Four equal passenger columns leave a floating label 97px of content box,
   because Bootstrap pads it .75rem on both sides. "Infant in seat" wanted 98.

   The first attempt shrank the qualifier to buy the difference. That was
   tuning a font size to win pixels, and it did not survive the next change:
   swapping Arial for Plus Jakarta Sans ate the margin again. So the fix is
   the constraint itself -- reclaim the right-hand padding, which the label
   does not need. Its left edge still lines up with the field's own text, and
   the result no longer depends on which face is loaded.

   All four of these labels had the wrong `for` value before this, so none of
   them was reachable by click or by a screen reader anyway. */
.form-floating > label { padding-right: .25rem; }

/* ==========================================================================
   #GRID
   ========================================================================== */
.row {
  display: -webkit-box;
  display: -webkit-flex;
  display: -moz-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-flex-wrap: wrap;
  -ms-flex-wrap: wrap;
  flex-wrap: wrap;
  position: relative;
}

.row-space {
  -webkit-box-pack: justify;
  -webkit-justify-content: space-between;
  -moz-box-pack: justify;
  -ms-flex-pack: justify;
  justify-content: space-between;
}

.row-refine {
  margin: 0 -15px;
}

.col-2 {
  width: -webkit-calc((100% - 20px) / 2);
  width: -moz-calc((100% - 20px) / 2);
  width: calc((100% - 20px) / 2);
}

@media (max-width: 767px) {
  .col-2 {
    width: 100%;
  }
}

/* ==========================================================================
   #BOX-SIZING
   ========================================================================== */
/**
 * More sensible default box-sizing:
 * css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice
 */
html {
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
}

* {
  padding: 0;
  margin: 0;
}

*, *:before, *:after {
  -webkit-box-sizing: inherit;
  -moz-box-sizing: inherit;
  box-sizing: inherit;
}

/* ==========================================================================
   #RESET
   ========================================================================== */
/**
 * A very simple reset that sits on top of Normalize.css.
 */
body,
h1, h2, h3, h4, h5, h6,
blockquote, p, pre,
dl, dd, ol, ul,
figure,
hr,
fieldset, legend {
  margin: 0;
  padding: 0;
}

/**
 * Remove trailing margins from nested lists.
 */
li > ol,
li > ul {
  margin-bottom: 0;
}

/**
 * Remove default table spacing.
 */
table {
  border-collapse: collapse;
  border-spacing: 0;
}

/**
 * 1. Reset Chrome and Firefox behaviour which sets a `min-width: min-content;`
 *    on fieldsets.
 */
fieldset {
  min-width: 0;
  /* [1] */
  border: 0;
}

button {
  outline: none;
  background: none;
  border: none;
  font-family: inherit;
}

a {
  cursor: pointer;
}

/* ==========================================================================
   #SKIP LINK
   ==========================================================================
   Off-screen until it takes focus, then the first thing on the page.

   Not Bootstrap's .visually-hidden-focusable, which is already loaded and
   would have done: it un-hides the link in normal flow, so taking focus pushes
   the whole page down by its height and the reader's first act on the page is
   watching it jump. Fixed and translated costs no layout at all.

   translateY by percentage, which is a percentage of the element's own height,
   plus the offset it sits at when shown -- so it clears the top edge
   completely instead of leaving a sliver of itself visible. A number here
   would be a guess at the height of one line of 14px text plus its padding.

   :focus and not :focus-visible. There is nothing to be careful about: the
   link is invisible until focused, so the only way to reach it is the keyboard
   anyway, and the heuristic that separates the two has no work to do.

   White on navy rather than the navy pill this started as. The header is
   `bg-primary` on every page in the app, so this always arrives on top of the
   navy -- and a navy pill there has no edge of its own: what you saw was the
   focus ring doing the pill's job, which is a link that vanishes the moment
   anything touches the ring. White measures 15.60 against that header, and it
   is the same white sheet the rest of the app puts on a dark ground.

   The ring is the footer's, for the footer's reason: --brand-accent is a dark
   teal on a dark blue, and the gap the offset opens shows the navy through. */
.skip-link {
  position: fixed;
  top: .75rem;
  left: .75rem;
  z-index: 1070;
  padding: .625rem 1rem;
  border-radius: .5rem;
  background-color: var(--surface-raised);
  color: var(--brand-ink);
  font-size: .875rem;
  font-weight: 600;
  text-decoration: none;
  transform: translateY(calc(-100% - .75rem));
}

.skip-link:focus {
  transform: none;
  outline: 2px solid var(--ink-on-dark-lead);
  outline-offset: 2px;
}

/* Where it lands. No ring: the wrapper is focusable only so the cursor
   follows the link, and a ring drawn around the whole of a page's content
   would say "you have selected this" -- the same call .help-group:focus makes,
   for the same reason. What tells a sighted reader it worked is the link
   itself disappearing and the next Tab landing inside the content. */
.page:focus { outline: none; }

/* ==========================================================================
   #PAGE WRAPPER
   ========================================================================== */
.page-wrapper {
  min-height: 100vh;
}
/* The ground every page stands on.

   White was the default because nothing had ever said otherwise, and it left
   the white cards the app is built out of invisible against it -- a booking
   block, a flight card and a footer column all have a white surface and no
   edge, so on white they read as one continuous sheet. A step of grey behind
   them is what makes each one a thing.

   On body and not on .page, so it also reaches the bands that are not page
   content: the breadcrumb strip sits above .page and was left a white seam
   between the navy header and the grey below it. The header and the footer
   paint their own colour over this, so there is nothing to fight. */
body {
  background-color: var(--surface);
}


/* What a rubber-band scroll reveals past either end of the page.

   The canvas was white, so overscrolling at the foot showed a white strip under
   the footer, and at the top the sticky header rode up with the document while
   the pinned search bar stayed at the viewport -- opening a white gap between
   two things that are meant to read as one band. The header's own colour closes
   that: the gap fills with the colour on both sides of it. Under the footer it
   used to be navy against Bootstrap's #212529 -- a seam between two dark greys
   rather than a flash of white, which was the best that could be said for it.
   The footer is this colour now, so there is no seam at either end. */
html { background-color: var(--brand-ink-fill); }

body {
  /* Tall enough to push the footer down on a short page, and no taller.
     Bootstrap's .h-100 was doing the first half and breaking the second: it
     gives the body a box exactly one viewport tall whatever it holds, and a
     sticky child only sticks within its parent's box -- so the header came
     unstuck a screen down and the page ended with a pinned search bar and
     nothing above it. min-height grows with the content; the sticky footer
     still works, because that is `d-flex flex-column` here and `mt-auto` on
     the footer, neither of which needed the fixed height. */
  min-height: 100vh;
  /* Falls back to the platform UI face rather than Arial, so a failed webfont
     lands somewhere deliberate. */
  font-family: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont,
               "Segoe UI", system-ui, sans-serif;
  /* Prices, times and durations sit in columns down the results list, and
     proportional digits make those columns ragged. */
  font-variant-numeric: tabular-nums;
  font-weight: 400;
  font-size: 14px;
}

/* h1, h2, h3, h4, h5, h6 {
  font-weight: 400;
}

h1 {
  font-size: 36px;
}

h2 {
  font-size: 30px;
}

h3 {
  font-size: 24px;
}

h4 {
  font-size: 18px;
}

h5 {
  font-size: 15px;
}

h6 {
  font-size: 13px;
} */

img {
  max-width: 100%;
  height: auto;
}

a {
  display: inline-block;
  text-decoration: none;
  /* Colour only: `all` also animated layout and paint properties that no
     anchor state here changes. .12s to match the rest of the file's hover
     feedback -- 0.4s was long enough to read as lag on a link. */
  transition: color var(--dur-fast) ease;
}

a:link {
  text-decoration: none;
}

a:visited {
  text-decoration: none;
}

a:hover {
  text-decoration: none;
}

a:active {
  text-decoration: none;
}

/* ==========================================================================
   #BACKGROUND
   ========================================================================== */
.bg-black {
  background-color: #000;
}

/* ==========================================================================
   #SPACING
   ========================================================================== */
.p-t-5 {
  padding-top: 5px;
}

.p-t-15 {
  padding-top: 15px;
}

.p-t-30 {
  padding-top: 30px;
}

.p-t-275 {
  padding-top: 275px;
}

@media (max-width: 767px) {
  .p-t-275 {
    padding-top: 120px;
  }
}

.p-t-200 {
  padding-top: 200px;
}

@media (max-width: 767px) {
  .p-t-200 {
    padding-top: 150px;
  }
}

.p-t-165 {
  padding-top: 165px;
}

@media (max-width: 767px) {
  .p-t-165 {
    padding-top: 120px;
  }
}

.p-t-80 {
  padding-top: 80px;
}

@media (max-width: 767px) {
  .p-t-80 {
    padding-top: 60px;
  }
}

.p-t-100 {
  padding-top: 100px;
}

@media (max-width: 767px) {
  .p-t-100 {
    padding-top: 80px;
  }
}

.p-b-5 {
  padding-bottom: 5px;
}

.p-b-80 {
  padding-bottom: 80px;
}

@media (max-width: 767px) {
  .p-b-80 {
    padding-bottom: 60px;
  }
}

.p-b-100 {
  padding-bottom: 100px;
}

@media (max-width: 767px) {
  .p-b-100 {
    padding-bottom: 90px;
  }
}

.p-b-120 {
  padding-bottom: 120px;
}

@media (max-width: 767px) {
  .p-b-120 {
    padding-bottom: 250px;
  }
}

.m-r-45 {
  margin-right: 45px;
}

@media (max-width: 767px) {
  .m-r-45 {
    margin-right: 15px;
  }
}

.p-r-5 {
  padding-right: 5px;
}

.p-l-5 {
  padding-left: 5px;
}

/* ==========================================================================
   #WRAPPER
   ========================================================================== */
.wrapper {
  margin: 0 auto;
}

.wrapper--w768 {
  max-width: 768px;
}

/* ==========================================================================
   #DATEPICKER
   ==========================================================================

   Ours, and no longer a vendor's restyled. It replaced daterangepicker and with
   it moment -- 26KB gzipped for a month grid and some date arithmetic, around
   which sat as much of our code again fighting its interaction model.

   The range reads as one pill: the days between are tinted and square-edged so
   they run together, and only the two ends are round. A single day is both ends
   at once, so it is a circle. */

.datepicker {
  position: absolute;
  z-index: 30;
  padding: .5rem;
  font-family: inherit;
  background-color: var(--surface-raised);
  border: 1px solid var(--line);
  border-radius: 1rem;
  box-shadow: 0 1rem 2.5rem rgba(21, 26, 33, .16);
}

.datepicker__months { display: flex; }
.datepicker__month { padding: .5rem .75rem; }

.datepicker__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  padding-bottom: .5rem;
}

.datepicker__title {
  flex: 1 1 auto;
  margin: 0;
  font-size: .9375rem;
  font-weight: 700;
  text-align: center;
  color: var(--ink-body);
}

/* Round, quiet, and out of the way until wanted -- the months are the heading,
   not the arrows. Drawn rather than lettered, so they do not depend on a font
   the page may not have finished loading. */
.datepicker__nav {
  flex: 0 0 auto;
  width: 2rem;
  height: 2rem;
  padding: 0;
  background: none;
  border: none;
  border-radius: 50%;
  transition: background-color var(--dur-fast) ease;
}

.datepicker__nav:hover { background-color: var(--surface); }

.datepicker__nav:focus-visible {
  outline: 2px solid var(--brand-accent);
  outline-offset: 2px;
}

.datepicker__nav::before {
  content: "";
  display: inline-block;
  width: .5rem;
  height: .5rem;
  border: solid var(--ink-secondary);
  border-width: 0 2px 2px 0;
}

.datepicker__nav--prev::before { transform: rotate(135deg) translate(-.0625rem, -.0625rem); }
.datepicker__nav--next::before { transform: rotate(-45deg) translate(-.0625rem, -.0625rem); }

.datepicker__grid {
  border-collapse: collapse;
  table-layout: fixed;
}

/* min-width and line-height, not width and height: on a table cell those two
   are hints the layout algorithm is free to ignore. The line height is what
   actually gives a day its room. */
.datepicker__grid th,
.datepicker__grid td {
  min-width: 2.375rem;
  padding: 0;
  line-height: 2.375rem;
  text-align: center;
}

/* Roomier where fares belong -- a four-figure price does not fit the width a
   two-digit day needs.

   Fixed, not fitted. The height is set whether or not that day has a fare and
   whether or not any have arrived yet, because a grid that grows a line when
   the prices land moves every day out from under the pointer. */
.datepicker--priced .datepicker__grid th,
.datepicker--priced .datepicker__grid td { min-width: 3.5rem; }

.datepicker--priced .datepicker__grid td {
  height: 3rem;
  padding: .25rem 0;
  line-height: 1;
  vertical-align: middle;
}

.datepicker__grid th {
  font-size: .75rem;
  font-weight: 600;
  color: var(--ink-secondary);
}

.datepicker__grid th abbr {
  text-decoration: none;
  border: none;
  cursor: inherit;
}

.datepicker__grid th.is-weekend { color: var(--ink-faint); }

/* Two lines once there are prices: the day, and what a seat costs that day.
   The cell has to grow to hold them, which is most of why the reference's
   calendar is so much wider than a plain one. */
.datepicker__grid td {
  position: relative;
  padding: .125rem 0;
  font-size: .875rem;
  color: var(--ink-body);
  cursor: pointer;
  user-select: none;                /* a drag would otherwise select the numbers */
  border-radius: 0;
  transition: background-color var(--dur-fast) ease, color var(--dur-fast) ease;
}

.datepicker__grid td:hover:not(.is-disabled):not(.is-in-range) {
  background-color: var(--surface);
  border-radius: .5rem;
}

.datepicker__grid td:focus-visible {
  outline: 2px solid var(--brand-accent);
  outline-offset: -2px;
  border-radius: .5rem;
}

/* A day in another month, or one already gone. */
.datepicker__grid td.is-outside { color: var(--ink-faint); }

.datepicker__grid td.is-disabled {
  color: var(--ink-faint);
  cursor: default;
}

/* Today, ringed. It was an underline, which -- on a span that fills its cell --
   drew a rule the width of the whole column and read as an artefact rather than
   as a mark on a date. Not on a day that is already chosen: there the accent
   fill says everything, and a ring inside it is noise. */
.datepicker__grid td.is-today:not(.is-start):not(.is-end) {
  /* ink-faint, not a rule colour: this says which day is today, and the rule
     tier measures 1.4:1 on white -- under the 3:1 a mark carrying meaning has
     to clear before it is worth drawing at all. */
  box-shadow: inset 0 0 0 1px var(--ink-faint);
  border-radius: .5rem;
}

/* The day is what is being chosen, so it is what the eye lands on. */
.datepicker__day {
  display: block;
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.25;
}

.datepicker--priced .datepicker__grid td.is-outside .datepicker__day { font-weight: 400; }

/* And the fare is what helps choose it, so it stays underneath in every sense:
   smaller, lighter, and a tier quieter in colour.

   The line is kept whether or not there is a fare to put on it. An empty block
   generates no line box at all, so a day without a price used to sit lower than
   its neighbours -- the numbers along a row stopped lining up wherever the route
   had nothing to sell. */
.datepicker__price {
  display: block;
  min-height: .825rem;
  font-size: .6875rem;
  font-weight: 400;
  line-height: 1.2;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}

/* Within a tenth of the cheapest fare on the route. Green carries the meaning
   here, so the weight carries it too -- colour alone would say nothing to
   anyone who cannot separate these two greens. */
.datepicker__grid td.is-cheap .datepicker__price {
  font-weight: 700;
  color: var(--brand-success);
}

/* On the chosen days the pill is already accent-filled, so the fare has to sit
   on that rather than on the page. */
/* The ends are accent-filled and the middle is tinted, so the fare needs a
   different colour on each. Excluded rather than ordered: both rules match the
   ends of a one-day window, and whichever came last would win by accident. */
.datepicker__grid td.is-in-range:not(.is-start):not(.is-end) .datepicker__price {
  color: var(--ink-secondary);
}

.datepicker__grid td.is-start .datepicker__price,
.datepicker__grid td.is-end .datepicker__price { color: rgba(255, 255, 255, .85); }

/* The middle of the range: square, so consecutive days join up. */
.datepicker__grid td.is-in-range {
  background-color: var(--brand-tint);
  border-radius: 0;
}

/* The ends, which are the only round corners. */
.datepicker__grid td.is-start,
.datepicker__grid td.is-end {
  color: var(--on-brand);
  background-color: var(--brand-accent-fill);
}

/* The leg not being edited, and the days the trip covers between the two.

   Both are behind the active window in every sense -- paler fill, no white
   text -- because they are context for the choice being made rather than the
   choice itself. Declared before the active rules so those win outright on a
   day that happens to be in both. */
.datepicker__grid td.is-between { background-color: var(--surface); }

.datepicker__grid td.is-other {
  color: var(--ink-body);
  background-color: var(--brand-tint);
}

.datepicker__grid td.is-other-start { border-radius: .75rem 0 0 .75rem; }
.datepicker__grid td.is-other-end { border-radius: 0 .75rem .75rem 0; }
.datepicker__grid td.is-other-start.is-other-end { border-radius: .75rem; }

.datepicker__grid td.is-other .datepicker__price { color: var(--ink-secondary); }

.datepicker__grid td.is-start { border-radius: .75rem 0 0 .75rem; }
.datepicker__grid td.is-end { border-radius: 0 .75rem .75rem 0; }
.datepicker__grid td.is-start.is-end { border-radius: .75rem; }

.datepicker__foot {
  padding: .5rem .75rem .25rem;
  text-align: center;
  border-top: 1px solid var(--line);
}

.datepicker__apply {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .0625rem;
  width: 100%;
  padding: .5rem 1rem;
  font-size: .9375rem;
  font-weight: 600;
  color: var(--on-brand);
  background-color: var(--brand-accent-fill);
  border: none;
  border-radius: .625rem;
  transition: background-color var(--dur-fast) ease, transform var(--dur-fast) ease;
}

/* What the chosen days cost, under what the button does. Empty until the fares
   arrive, and it takes no room while it is. */
.datepicker__apply-price {
  font-size: .75rem;
  font-weight: 500;
  color: rgba(255, 255, 255, .85);
}

.datepicker__apply-price:empty { display: none; }

.datepicker__apply:hover:not(:disabled) { background-color: var(--brand-accent-fill-700); }
.datepicker__apply:active:not(:disabled) { transform: scale(.98); }
.datepicker__apply:disabled { opacity: .55; }

.datepicker__apply:focus-visible {
  outline: 2px solid var(--brand-accent-700);
  outline-offset: 2px;
}

/* The two arrows that say the window can be pulled wider. They sit just outside
   the ends of the range rather than on them, where they read as something to
   grab instead of decoration on a chosen day -- there is room, because the
   month is padded past its last column.

   Drawn rather than inserted, so nothing has to be rebuilt as the range moves.
   Only where a window can actually be dragged: the rebook dialog's calendar
   spans a departure and a return, which these cannot move. */
.datepicker--drag .datepicker__grid td.is-start::before,
.datepicker--drag .datepicker__grid td.is-end::after {
  content: "";
  position: absolute;
  top: 50%;
  width: .3125rem;
  height: .3125rem;
  border: solid var(--brand-accent);
  border-width: 2px 2px 0 0;
  pointer-events: none;
}

.datepicker--drag .datepicker__grid td.is-start::before {
  left: -.4375rem;
  transform: translateY(-50%) rotate(-135deg);
}

.datepicker--drag .datepicker__grid td.is-end::after {
  right: -.4375rem;
  transform: translateY(-50%) rotate(45deg);
}

.datepicker--drag .datepicker__grid td.is-start,
.datepicker--drag .datepicker__grid td.is-end { cursor: ew-resize; }

/* Lifted out of the dialog's flow, though not out of the dialog.

   A scrollable modal clips its own content -- Bootstrap gives `.modal-content`
   overflow:hidden so the body can scroll -- which cut the calendar off at the
   dialog's edge. Fixed to the viewport, it is clipped by nothing and has the
   whole screen to open in. 1060 draws it over the dialog it still belongs to,
   which Bootstrap puts at 1055. */
.datepicker--overlay {
  position: fixed;
  z-index: 1060;
}

/* One month under the other on a phone: side by side they do not fit. */
@media (max-width: 575.98px) {
  .datepicker__months { flex-direction: column; }
  .datepicker__month { padding: .5rem .25rem; }
  .datepicker__grid { width: 100%; }
  .datepicker__grid th,
  .datepicker__grid td { min-width: 2rem; line-height: 2rem; }
  .datepicker--priced .datepicker__grid th,
  .datepicker--priced .datepicker__grid td { min-width: 2.875rem; }
  .datepicker--priced .datepicker__grid td { height: 2.75rem; }
  .datepicker__day { font-size: .9375rem; }
}


/* ==========================================================================
   #FORM
   ========================================================================== */
input,
textarea {
  outline: none;
  margin: 0;
  border: none;
  -webkit-box-shadow: none;
  -moz-box-shadow: none;
  box-shadow: none;
  width: 100%;
  font-size: 18px;
  font-family: inherit;
}

input:disabled {
  background: transparent;
  cursor: pointer;
}

textarea {
  resize: none;
}

button {
  cursor: pointer;
}


/* CHECKBOX */
/* Radio button */


/* ==========================================================================
   #HOMEPAGE HERO
   ==========================================================================

   A flat band carrying the slogan, the two sections and the search bar. The
   colour is the app's own ink rather than the reference's blue, and it is very
   slightly graded so a full-width rectangle does not read as a dead block. */

/* One flat colour, the header's own. The band was very slightly graded so that
   a full-width rectangle would not read as a dead block; with the header stuck
   above it and the search bar pinned into it, all three are the same surface
   now, and a gradient meeting a flat colour at two seams looked like three
   panels rather than one. */
.hero { background-color: var(--brand-ink-fill); }

.hero__inner {
  padding-top: 5.5rem;
  /* Half the gap to the search bar. The other half is the bar's own top
     padding, and the split is what lets the gap differ at rest and pinned: all
     of this scrolls away, and only the bar's half is left under the header. */
  padding-bottom: 1.25rem;
}

/* The destination cards, starting inside the band and finishing outside it.

   Positioned so the overlap is unambiguous rather than relying on paint order,
   and at a z-index the place dropdown still clears -- that panel opens downward
   past the edge of the band and has to stay on top of the cards it crosses. */
.hero-cards {
  position: relative;
  z-index: 1;
  /* A sixth of a card, roughly -- enough for the band to cut across it rather
     than sit behind it. Deeper read as the cards being half-swallowed. */
  margin-top: -3.75rem;
  padding-bottom: 3rem;
}

@media (max-width: 767.98px) {
  /* Less of both: the band is already tall once the fields stack, and a card
     is most of the screen, so a deep overlap buries its own heading. */
  .hero__inner { padding-top: 3rem; padding-bottom: 7.5rem; }
  .hero-cards { margin-top: -2.5rem; }
}

/* Holds the tray's height while the tray is up in the header, so nothing below
   it moves when it leaves. The height itself is written by global.js from the
   tray's own.

   The gap to the search bar belongs here rather than on the tray. A child's
   bottom margin only escapes a parent whose height is `auto`, and this one's is
   pinned by that min-height -- so on the tray the 1.75rem was swallowed by the
   slot's own box the moment the height was written, and the bar jumped up to
   sit against the tray. On the slot it is outside the pinned box and cannot be
   eaten. */
.hero__modes-slot { position: relative; }

/* The slot's bottom edge, which is where the tray finishes going under the
   header. Taken out of the flow and pinned there: as a flow child it would ride
   up to the top of the slot the moment the tray left, moving the very line that
   decides whether the tray should have left -- which is a switch that flips
   itself back and forth for as long as you hold still. */
.hero__modes-sentinel {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
}

/* The tray, docked into the header.

   Laid out along the row rather than down it: in the hero each section is a
   tile with its icon above its name and room to breathe, and in a 88px header
   that is a tile too tall to fit and too loud beside a logo. Same tray, same
   order, one line. */
.hero__modes.is-docked { margin: 0; gap: .25rem; }
.hero__modes.is-docked .hero__modes-group { padding: .1875rem; border-radius: .625rem; }
.hero__modes.is-docked .hero__mode {
  flex-direction: row;
  gap: .5rem;
  min-width: 0;
  padding: .4375rem .75rem;
  font-size: .8125rem;
}
.hero__modes.is-docked .hero__mode i { font-size: .875rem; }

/* The band, condensed.

   Scrolling the homepage leaves the menu and the search bar behind and takes
   the rest of the hero with it -- the slogan and the sections have said what
   they had to say by the time a visitor is reading destinations, and the one
   thing they may still want is the field they were typing in.

   The bar is pinned rather than copied. The form is wired to literal element
   ids (see search/form.html.twig), so a second copy on the page would fight the
   first over every one of them; and it is pinned rather than moved in the DOM,
   because moving a field that has an open dropdown attached to it loses the
   dropdown. The header above is sticky, so the two sit together.

   --header-h is written by global.js, which measures the header rather than
   guessing: it is a navbar that wraps at some widths and not others. */
/* Sticky, not pinned by script. A scroll is run on the compositor and a scroll
   handler on the main thread, so anything driven from JS is a frame behind the
   page it is reacting to -- on a flick the bar appeared where the scroll had
   got to and then dropped into place. Sticky is resolved where the scrolling
   happens and cannot lag it.

   Its own band, in the hero's colour, so at rest it reads as the foot of the
   hero and pinned it reads as the underside of the header. */
.hero__bar {
  position: sticky;
  top: var(--header-h, 4.5rem);
  z-index: 1020;
  /* Tight to the menu once it is against it: the two are one band there, and a
     gap between them reads as the bar having come loose. The rest of the gap it
     has at rest is the hero's bottom padding, which scrolls away. */
  padding: .5rem 0 .75rem;
  background-color: var(--brand-ink-fill);
}

/* The band's remaining depth, below the bar, out of which the cards are pulled.
   Padding on the bar would make the sticky strip this tall. */
.hero__tail { height: 10.25rem; background-color: var(--brand-ink-fill); }

/* Nothing here resizes the form. It was compacted to the results page's
   metrics when pinned, which made the fields jump the moment they stuck -- the
   thing somebody may be part-way through typing into should not change shape
   underneath them. It keeps its own size, and only the band behind it changes. */

@media (max-width: 767.98px) {
  /* The fields stack here, so a pinned bar would be most of the screen. It
     scrolls away with the rest of the band. */
  .hero__bar { position: static; }
}

.hero__slogan {
  max-width: 20ch;
  text-align: center;
  margin: 0 auto 2rem;
  font-size: clamp(1.75rem, 4.5vw, 3rem);
  font-weight: 700;
  line-height: 1.12;
  letter-spacing: -.02em;
  color: var(--on-brand);
  text-wrap: balance;
}

/* What this app does, and what it is going to do. The sections sit in one
   tinted tray with the current one lifted out of it as a solid card -- the same
   figure/ground the results steps use. Business travel sits outside the tray,
   because it is a different audience rather than another thing to book. */
.hero__modes {
  display: flex;
  justify-content: center;
  gap: .5rem;
}

.hero__modes-group {
  display: flex;
  gap: .25rem;
  padding: .25rem;
  background-color: rgba(255, 255, 255, .1);
  border-radius: 1rem;
}

.hero__mode {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .375rem;
  min-width: 6.5rem;
  padding: .75rem 1.25rem;
  font-size: .875rem;
  font-weight: 600;
  color: var(--on-brand);
  text-decoration: none;
  white-space: nowrap;
  background-color: transparent;
  border-radius: .75rem;
  transition: background-color var(--dur-fast) ease, transform var(--dur-fast) ease;
}

/* Somewhere this app will go but has not yet. Dimmed rather than greyed out:
   60% white measures 5.2:1 on the tray, which is a section that is legibly
   there and legibly not ready -- not one nobody can read. */
.hero__mode.is-soon {
  color: rgba(255, 255, 255, .6);
  cursor: default;
}

.hero__mode.is-soon i { opacity: .8; }

/* A shade lighter than the group's, so the break between the two reads as two
   trays rather than as one with a gap torn in it. The muted label still
   measures 4.6:1 against it. */
.hero__modes-group--aside { background-color: rgba(255, 255, 255, .16); }

.hero__mode i { font-size: 1.125rem; }

a.hero__mode:hover {
  color: var(--on-brand);
  background-color: rgba(255, 255, 255, .18);
}

a.hero__mode:active { transform: scale(.97); }

.hero__mode.is-current {
  color: var(--brand-ink);
  background-color: var(--surface-raised);
}

/* Six sections do not fit a phone. Scrolled rather than wrapped -- wrapping
   costs two more rows of a band that is already tall once the fields stack --
   and rather than squashed, because "For business" cannot be read at a sixth of
   375px.

   flex-start, not the centre the wider layout uses: content centred inside a
   scroller overflows both ways, and whatever ends up left of the origin cannot
   be scrolled back to. Here that was Flights, the current page. */
@media (max-width: 767.98px) {
  .hero__modes {
    justify-content: flex-start;
    overflow-x: auto;
    scrollbar-width: none;
  }

  .hero__modes::-webkit-scrollbar { display: none; }

  .hero__modes-group { flex: 0 0 auto; }
  .hero__mode { min-width: 5rem; padding: .625rem .75rem; font-size: .8125rem; }
}

@media (max-width: 575.98px) {
  /* Top only. The bottom belongs to the pair above -- what separates the search
     button from the first card is that padding less the cards' overlap, so
     cutting one here on its own draws a card across the button. */
  .hero__inner { padding-top: 2.25rem; }
}

/* ==========================================================================
   #SEARCHBAR
   ==========================================================================

   The search form, which never had a block of its own -- it was Bootstrap
   utilities stacked in a column, in a codebase where every other area has one.
   One horizontal bar: origin, destination, depart, return, who is flying, go.

   The fields sit in a single white surface divided by rules rather than as
   separate boxes, so the row reads as one control. Below 992px they stack, and
   the dividers become the gaps between them. */

.searchbar__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  /* The bar draws nothing. It used to have a white gutter running round the
     whole row -- which read as a border about the form, and tied the button
     into the same object as the fields. The dividers it drew as 1px gaps are
     borders on the fields now, for want of anything behind them. */
  gap: 0;
  padding: 0;
  background-color: transparent;
  border-radius: 0;
}

.searchbar__field {
  position: relative;             /* the autocomplete panel anchors to this */
  display: flex;
  /* Basis 0, not a width: the fields share what is left after the button, so
     the row always fits on one line instead of wrapping the submit off the end
     the moment an airport name gets long. */
  flex: 1 1 0;
  flex-direction: column;
  justify-content: center;
  gap: 0;
  min-width: 9rem;
  min-height: 4rem;
  padding: .5rem 1.125rem;
  background-color: var(--surface-raised);
  /* The hairline between fields, drawn here because the bar behind them is
     transparent and has no colour to show through a gap. */
  border-left: 1px solid var(--line);
}

.searchbar__field:first-child {
  border-left: 0;
  border-radius: .75rem 0 0 .75rem;
}

/* Whichever field ends the white block, rather than counting to it: the row has
   been reordered twice already. */
.searchbar__field:has(+ .searchbar__submit) { border-radius: 0 .75rem .75rem 0; }

/* The row shared out by what each field actually has to say. A place is a city
   name with a code beside it, a date is "Oct 12 - Oct 14" once a window is
   chosen, and the party runs to "6 passengers, Business". An equal share left
   the last three ellipsing while the places sat on room they did not need. */
.searchbar__field--place { flex: 1.4 1 0; }
.searchbar__field--date { flex: 1.15 1 0; }
/* No `padding: 0` here. The field's own padding used to be outranked by the
   hero rule and never applied; folded together they tie, and the later one --
   this -- would start winning and flatten the party field against its edges. */
.searchbar__field--party { flex: 1.5 1 0; min-width: 12rem; }

/* Named for a screen reader and nothing else: the placeholder says what the
   field is until a value says it instead, and a placeholder disappears exactly
   when somebody tabbing back needs it most. */
.searchbar__label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  margin: 0;
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .02em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

.searchbar__input {
  width: 100%;
  padding: 0;
  font-size: 1.0625rem;
  color: var(--ink-body);
  background: none;
  border: none;
  outline: none;
  text-overflow: ellipsis;        /* "Montréal-Trudeau, Montreal, Canada" in 12rem */
}

.searchbar__input::placeholder { color: var(--ink-faint); }
.searchbar__input:read-only { cursor: pointer; }

/* The field the calendar is currently working on.

   A ring rather than a fill: the field still has to read as a field, and the
   two dates sit next to each other, so what is needed is a mark saying which of
   them a click in the calendar will move -- not a second selected-looking
   thing. Drawn inside, because the fields are butted together with a one-pixel
   rule between them and an outline would sit on top of its neighbour.

   No radius of its own. It had one from when each field carried its own frame;
   the bar is one white block now, and a rounded box in the middle of it read as
   something dropped on top rather than one of the cells being marked. The ring
   takes whatever corners the field itself has, which is square in the middle of
   the row and rounded at the two ends. */
.searchbar__field.is-picking {
  box-shadow: inset 0 0 0 2px var(--brand-accent);
}

/* Outside the search bar -- the rebook dialog -- there is no field wrapper to
   mark, so the control itself carries it. */
.is-picking:not(.searchbar__field) { outline: 2px solid var(--brand-accent); outline-offset: 1px; }
.searchbar__field .is-picking { outline: none; }

/* The way back to a one-way trip, now that no tab does it. */
.searchbar__clear {
  position: absolute;
  top: 50%;
  right: .5rem;
  padding: .25rem .375rem;
  font-size: .75rem;
  line-height: 1;
  color: var(--ink-muted);
  background: none;
  border: none;
  border-radius: 50%;
  transform: translateY(-50%);
  transition: color var(--dur-fast) ease, background-color var(--dur-fast) ease;
}

.searchbar__clear:hover {
  color: var(--ink-body);
  background-color: var(--surface);
}

/* Set apart from the fields, which is what makes it read as the thing that
   acts rather than the last thing to fill in.

   It carries no edge of its own, by choice. The accent measures 2.85:1 against
   the ink band behind it, under the 3:1 that would let its boundary be found by
   contrast alone -- what identifies it here is its label, set in white on the
   fill at 10.3:1, and its position at the end of the row. */
.searchbar__submit {
  flex: 0 0 auto;
  margin-left: .5rem;
  padding: 0 2.25rem;
  font-size: 1.0625rem;
  font-weight: 600;
  color: var(--on-brand);
  background-color: var(--brand-accent-fill);
  border: none;
  border-radius: .75rem;
  transition: background-color var(--dur-fast) ease;
}

.searchbar__submit:hover { background-color: var(--brand-accent-fill-700); }

/* Stacked, the row's parts stop being a row. Each field is a card of its own,
   so the divider between them, the radii that opened and closed the white
   block, and the gap that set the button apart all have nothing left to do --
   and left alone they show up as a stray edge down one side of every card and
   an indent on a full-width button. */
@media (max-width: 991.98px) {
  .searchbar__field,
  .searchbar__field:first-child,
  .searchbar__field:has(+ .searchbar__submit) {
    border-left: 0;
    border-radius: var(--bs-border-radius);
  }

  /* And it takes the row it has been pushed onto. */
  .searchbar__submit { margin-left: 0; flex-basis: 100%; }
}

/* The code for whatever place is chosen, set to one side of the field the way
   the reference does. It is the least ambiguous name an airport has, and the
   field itself is too narrow to spell most of them out. */
.searchbar__code {
  position: absolute;
  top: 50%;
  right: 1.125rem;
  font-size: .8125rem;
  font-weight: 600;
  letter-spacing: .04em;
  color: var(--ink-faint);
  pointer-events: none;
  transform: translateY(-50%);
}

.searchbar__code:empty { display: none; }

/* Room for it, so a long place name is ellipsed before it collides.

   input, not the class on its own: the hidden native select carries the same
   class, and padding on a 1px-wide absolutely positioned element is still 3rem
   of box -- enough to push the page 9px wider than the phone showing it. */
.searchbar__field--place input.searchbar__input { padding-right: 3rem; }

/* Stacked rather than paired. Two of them side by side on a phone leaves each
   about 140px, which is not enough for a placeholder, let alone a place. */
@media (max-width: 767.98px) {
  .searchbar__field--place { flex-basis: 100%; }
}

/* The band the results-page form sits in. Replaces .card-4, which was a
   translucent black panel over whatever the page had behind it. */
/* The results page's own band, and the only thing held on this page.

   The search is what you are adjusting here -- scrolling a list of flights is
   exactly when you want to change a date -- and the menu is not. So the band
   takes the top of the screen for itself and the header goes with the page,
   which is why this sticks at 0 rather than under a header that is no longer
   there. See the rule below, which is what lets the header scroll.

   Sticky rather than pinned from script, for the reason the homepage's bar is:
   scrolling runs on the compositor and a scroll handler does not, so anything
   driven from JS arrives a frame late and drops into place. A direct child of
   the body, so the page is what bounds it. */
.searchbar-band {
  position: sticky;
  top: 0;
  z-index: 1020;
  padding: 1rem 0 1.25rem;
  background-color: var(--brand-ink-fill);
}

@media (max-width: 991.98px) {
  /* The fields stack below the navbar's breakpoint, and a band that tall
     against the top of the screen leaves no room for the results. */
  .searchbar-band { position: static; }
}

/* Who is flying: one trigger, and a panel holding the controls that submit.
   Four selects side by side do not fit a single-row bar, which is why the
   reference collapses them into one. */
.party { position: relative; height: 100%; }

.party__trigger {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: .125rem;
  width: 100%;
  height: 100%;
  padding: .5rem .875rem;
  text-align: left;
  background: none;
  border: none;
}

/* Who is flying, stacked. There is no standing label to say what the field is,
   so the count and the cabin become the two lines that name it. It is also what
   makes the longest party fit -- "9 passengers, Premium Economy" has no width at
   which it sits on one line beside four other fields. */
.party__summary {
  display: flex;
  flex-direction: column;
  gap: 0;
  overflow: hidden;
  font-size: .9375rem;
  line-height: 1.3;
  color: var(--ink-body);
  white-space: nowrap;
}

.party__class { font-size: .875rem; color: var(--ink-muted); }

.party__summary > span { overflow: hidden; text-overflow: ellipsis; }

/* Nothing between the count and the cabin: they are on their own lines. The
   comma this once carried was for the one-line layout the results page used to
   have, and it is written here rather than into the element because the text is
   rewritten on every change -- a separator in the markup would have to be
   rebuilt each time by a paint that should only care about the two facts. */
.party__heads::after { content: none; }

.party__panel {
  position: absolute;
  top: calc(100% + .5rem);
  right: 0;
  z-index: 20;
  width: 21rem;
  padding: .5rem;
  background-color: var(--surface-raised);
  border: 1px solid var(--line);
  border-radius: var(--bs-border-radius-lg);
  box-shadow: 0 .5rem 1.5rem rgba(21, 26, 33, .12);
}

/* In a dialog the panel opens in place rather than over the page. Floated, it
   escaped the modal entirely -- there is nothing below a modal to hang a panel
   in, so it hung across the backdrop instead. Expanding in flow lets the dialog
   grow around it, which is what a dialog is for. */
.modal .party { position: static; }

.modal .party__panel {
  position: static;
  width: auto;
  margin-top: .5rem;
  border-radius: var(--bs-border-radius);
  box-shadow: none;
}

/* The trigger is a field in here, not a slot in a bar. */
.modal .party__trigger {
  padding: .625rem .75rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--bs-border-radius);
}

.party__row {
  display: flex;
  gap: .75rem;
  align-items: center;
  justify-content: space-between;
  padding: .625rem .625rem;
}

.party__row + .party__row { border-top: 1px solid var(--line); }

.party__name {
  display: flex;
  flex-direction: column;
  margin: 0;
  padding: 0;
  font-size: .875rem;
  color: var(--ink-body);
}

.party__name small { font-size: .75rem; color: var(--ink-muted); }

/* The select still submits and still catches the empty-select disabler; the
   stepper is what anyone actually touches. Not `display: none`, so it keeps a
   label and stays reachable to anything reading the form. */
.party__native {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.party__stepper {
  display: flex;
  gap: .5rem;
  align-items: center;
}

.party__step {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  font-size: 1rem;
  line-height: 1;
  color: var(--brand-accent);
  background-color: var(--surface-raised);
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  transition: border-color var(--dur-fast) ease, color var(--dur-fast) ease;
}

.party__step:hover:not(:disabled) { border-color: var(--brand-accent); }
.party__step:active:not(:disabled) { transform: scale(.94); }

/* Dimmed rather than hidden: a control that vanishes at the limit gives no clue
   there was a limit. */
.party__step:disabled {
  color: var(--ink-faint);
  border-color: var(--line);
}

.party__value {
  min-width: 1.25rem;
  font-size: .9375rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink-body);
  text-align: center;
}

.party__cabins {
  margin: .25rem 0 0;
  padding: .5rem .625rem .25rem;
  border: none;
  border-top: 1px solid var(--line);
}

.party__cabins .party__name { padding: 0 0 .375rem; }

.party__cabin-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: .375rem;
}

/* The tile is the label, so the whole thing is the hit area. The radio itself
   is only visually gone -- it still submits, still takes focus, and still tells
   a screen reader what this is. */
.party__cabin {
  position: relative;             /* holds its own hidden input */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: .5rem .375rem;
  font-size: .8125rem;
  color: var(--ink-secondary);
  text-align: center;
  background-color: var(--surface);
  border: 1px solid transparent;
  border-radius: var(--bs-border-radius);
  cursor: pointer;
  transition: background-color var(--dur-fast) ease, color var(--dur-fast) ease;
}

.party__cabin input {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

.party__cabin:hover { background-color: var(--surface-sunken); }

.party__cabin:has(input:checked) {
  font-weight: 600;
  color: var(--brand-accent-700);
  background-color: var(--brand-tint);
  border-color: var(--brand-accent);
}

/* Focus lands on the input, which is not visible -- so the tile wears it. */
.party__cabin:has(input:focus-visible) { outline: 2px solid var(--brand-accent); outline-offset: 2px; }

/* What is left of the compact bar's own small-screen rules. The rest of them --
   a gap on the bar, a full-width basis on every field, a squarer button -- never
   reached the page: the modifier that carried the tall form outranked this
   block, and with the two folded together they would have started applying and
   turned a row that wraps on its own into one field per line. The fields carry
   `min-width: 9rem`, which is what wraps them, and the block above this one
   squares off their corners and gives the button its own row. */
@media (max-width: 991.98px) {
  .party__panel { width: 100%; }
}

/* ==========================================================================
   #SELECT
   ========================================================================== */

input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* ==========================================================================
   #SIDEBAR
   ========================================================================== */

.form-check-input:checked+.form-checked-content {
  opacity: .5;
}

.form-check-input-placeholder {
  border-style: dashed;
}

[contenteditable]:focus {
  outline: 0;
}

.list-group-checkable .list-group-item {
  cursor: pointer;
}

.list-group-item-check {
  position: absolute;
  clip: rect(0, 0, 0, 0);
}

.list-group-item-check:hover+.list-group-item {
  background-color: var(--bs-light);
}

.list-group-item-check:checked+.list-group-item {
  color: var(--on-brand);
  background-color: var(--bs-blue);
}

.list-group-item-check[disabled]+.list-group-item,
.list-group-item-check:disabled+.list-group-item {
  pointer-events: none;
  filter: none;
  opacity: .5;
}

.list-group-radio .list-group-item {
  cursor: pointer;
  border-radius: .5rem;
}

.list-group-radio .form-check-input {
  z-index: 2;
  margin-top: -.5em;
}

.list-group-radio .list-group-item:hover,
.list-group-radio .list-group-item:focus {
  background-color: var(--bs-light);
}

.list-group-radio .form-check-input:checked+.list-group-item {
  background-color: var(--bs-body);
  border-color: var(--bs-blue);
  box-shadow: 0 0 0 2px var(--bs-blue);
}

.list-group-radio .form-check-input[disabled]+.list-group-item,
.list-group-radio .form-check-input:disabled+.list-group-item {
  pointer-events: none;
  filter: none;
  opacity: .5;
}

/* ==========================================================================
   #TITLE
   ========================================================================== */
.title {
  font-size: 50px;
  font-weight: 700;
  text-align: center;
  text-transform: uppercase;
  color: var(--on-brand);
  margin-bottom: 24px;
}

/* ==========================================================================
   #BADGES
   ========================================================================== */
.badge-flight {
  position: relative;
  top: 18px;
  left: -32px;
  z-index: 2;
}

.badge-flight .rounded-pill {
  position: relative;
  right: -16px;
}

.badge-flight {
  color: var(--bs-light);
}

.badge-price {
  background-color: var(--bs-success-text);
}

.badge-duration {
  background-color: var(--bs-primary-text);
}

.badge-departure_time {
  background-color: var(--bs-info-text);
}

.badge-arrival_time {
  background-color: var(--bs-info);
}

.badge-rating {
  background-color: var(--bs-danger-text);
}

/* The cabin a popular search was run in, shown only when it is not economy.
   Two rows in that list can now differ by nothing else, so without it the
   same route and dates would appear twice with no way to tell them apart. */
.search-cabin {
  display: inline-block;
  padding: .05rem .4rem;
  border: 1px solid var(--brand-tint-edge);
  border-radius: .25rem;
  background-color: var(--brand-tint);
  color: var(--brand-accent-700);
  font-size: .75rem;
  font-weight: 600;
  white-space: nowrap;
}

/* ==========================================================================
   #AIRLINES
   ========================================================================== */

/* An airline is the only listing on the site with no surface of its own -- the
   airports beside it use a Bootstrap .card and these never did, because the
   white page was doing the job. With the page grey they needed a sheet like
   everything else. */
.airline-panel {
  padding: 1.25rem;
  background-color: var(--surface-raised);
  border-radius: .75rem;
}

.feature-icon {
  width: 4rem;
  height: 4rem;
  border-radius: .75rem;
}

.icon-square {
  width: 3rem;
  height: 3rem;
  border-radius: .75rem;
}

.text-shadow-1 {
  text-shadow: 0 .125rem .25rem rgba(0, 0, 0, .25);
}

.text-shadow-2 {
  text-shadow: 0 .25rem .5rem rgba(0, 0, 0, .25);
}

.text-shadow-3 {
  text-shadow: 0 .5rem 1.5rem rgba(0, 0, 0, .25);
}

.card-cover {
  background-repeat: no-repeat;
  background-position: center center;
  background-size: cover;
}

.feature-icon-small {
  width: 3rem;
  height: 3rem;
}

/* ==========================================================================
   #README
   ========================================================================== */

/* The /about page is the README, so this is the one place in the app styling
   generic markdown -- headings, paragraphs, lists and links straight out of
   CommonMark, with no classes to hook. Everything is scoped under .readme so
   these bare element selectors cannot reach the rest of the app.

   Only the markdown is here. The sheet it sits on, the measure, and the
   headings, paragraphs, lists and links that any prose page has are under
   #ARTICLE, shared with /help -- `.readme` is the extra a document nobody here
   wrote needs on top: the heading levels the help articles have no use for, and
   the blocks only a README produces. Nothing below overlaps that section, so
   which of the two comes first in this file does not matter. */

/* h1 is one of those levels, and one the file no longer reaches: the page
   supplies its own heading, so the README's title sits above the about:start
   marker. Kept because a README can grow another one, and an unstyled heading
   here does not fail harmlessly -- see the note on h4. */
.readme h1 {
  margin-bottom: .75rem;
  color: var(--brand-ink);
  font-size: 1.75rem;
  font-weight: 700;
  letter-spacing: -.01em;
}

/* Every level the file can reach, not only the levels it uses today. Bootstrap
   sizes h4 at 1.5rem, which is larger than the h2 above -- so an unstyled h4
   read as the biggest heading on the page and inverted the whole hierarchy.
   The README drives this page, so a level it grows into later must not be able
   to break it either. */
.readme h4 {
  margin-top: 1.5rem;
  margin-bottom: .35rem;
  color: var(--brand-ink-700);
  font-size: 1rem;
  font-weight: 700;
}

/* Neither level appears in the README today. They hold body size and take an
   uppercase label treatment, so a level added later is still unmistakably a
   heading without being smaller than the text under it. */
.readme h5,
.readme h6 {
  margin-top: 1.25rem;
  margin-bottom: .35rem;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
}

.readme h5 {
  color: var(--ink-secondary);
}

.readme h6 {
  color: var(--ink-muted);
}

/* A feature is a heading and one line about it, so the line sits tight under
   its heading rather than floating between two. */
.readme h3 + p,
.readme h4 + p,
.readme h5 + p,
.readme h6 + p {
  margin-top: 0;
  color: var(--ink-secondary);
}

/* A heading whose section opens with a list, which is how the command
   reference is written. */
.readme h4 + ol,
.readme h4 + ul {
  margin-top: .5rem;
}

/* The same rhythm the shared body gives a paragraph or a list, for the blocks
   only a README produces. Missing pre and table here once left a code block
   with no bottom margin at all, so the paragraph after it sat flush against the
   block and read as part of it -- while the gap going in was a full 1rem, which
   made the asymmetry look deliberate. Anything block-level a README can grow
   belongs in this list. */
.readme pre,
.readme table,
.readme blockquote {
  margin-bottom: 1rem;
}

.readme li > ul,
.readme li > ol {
  margin-top: .35rem;
  margin-bottom: .35rem;
}

/* A step whose command sits in a block under it: the block lines up with the
   step's text rather than starting a new indent. */
.readme li > pre {
  margin-top: .5rem;
}

/* The "Built With" badges are inline images inside links; without this they sit
   on the text baseline and drag the line height around. */
.readme p > a > img {
  vertical-align: middle;
  margin: 0 .15rem .35rem 0;
}

.readme code {
  padding: .1rem .3rem;
  border-radius: .25rem;
  background-color: var(--surface-sunken);
  color: var(--brand-ink);
  font-size: .875em;
}

.readme pre {
  position: relative;
  overflow-x: auto;
  /* Room on the right for the copy button, so a long line scrolls under empty
     space rather than under the control. */
  padding: 1rem 3.25rem 1rem 1rem;
  border: 1px solid var(--line);
  border-radius: .5rem;
  background-color: var(--surface-sunken);
}

/* Copy button on a code block.
   Anchored to the block rather than the scrolling content, so it stays put
   while a long command scrolls beneath it. Dimmed rather than hidden until
   hover: a hover-only control does not exist on a touch screen, and one that
   appears from nothing is easy to miss. */
.readme__copy {
  position: absolute;
  top: .5rem;
  right: .5rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: .375rem;
  background-color: var(--surface);
  color: var(--ink-secondary);
  font-size: .8125rem;
  line-height: 1;
  opacity: .55;
  transition: opacity var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}

.readme pre:hover .readme__copy,
.readme__copy:hover,
.readme__copy:focus-visible {
  opacity: 1;
  border-color: var(--brand-accent);
  color: var(--brand-accent-700);
}

/* Press feedback, the same 0.97 the rest of the app's controls use. */
.readme__copy:active {
  transform: scale(.94);
}

/* Held for a moment after a successful copy, so the tick is not gone before it
   is noticed. */
.readme__copy[data-state="done"] {
  opacity: 1;
  border-color: var(--brand-success);
  color: var(--brand-success);
}

.readme__copy[data-state="failed"] {
  opacity: 1;
  border-color: var(--brand-danger);
  color: var(--brand-danger);
}

.readme pre code {
  padding: 0;
  background: none;
}

/* A README written on GitHub can carry a table; it has to be able to scroll on
   a narrow screen rather than widen the page. */
.readme table {
  display: block;
  overflow-x: auto;
  max-width: 100%;
  border-collapse: collapse;
}

.readme th,
.readme td {
  padding: .4rem .75rem;
  border: 1px solid var(--line);
  text-align: left;
}

.readme th {
  background-color: var(--surface-sunken);
  font-weight: 600;
}

/* Neither appears in the README today. Bootstrap gives a blockquote nothing
   but margin, so an unstyled one is indistinguishable from a paragraph -- the
   same trap the unstyled h4 fell into. */
.readme blockquote {
  padding: .25rem 0 .25rem 1rem;
  border-left: 3px solid var(--brand-tint-edge);
  color: var(--ink-secondary);
}

.readme blockquote > :last-child {
  margin-bottom: 0;
}

.readme hr {
  margin: 2rem 0;
  border: 0;
  border-top: 1px solid var(--line);
  opacity: 1;
}

.readme img {
  max-width: 100%;
  height: auto;
}

button,
input[type="text"],
textarea {
  outline: none;
  box-shadow: none !important;
}

.accordion-item .accordion-button {
  color: var(--ink-body) !important;
}

/* "Flight details" toggle. A bordered pill rather than a text link: on a card
   already carrying links and icons, an underlined phrase did not read as the
   one control that opens the itinerary. Swaps its label and rotates the
   chevron by state. */
.details-toggle {
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  padding: .25rem .85rem;
  color: var(--ink-secondary);
  background-color: var(--surface-raised);
  font-weight: 500;
  transition: background-color var(--dur-fast) ease, border-color var(--dur-fast) ease, color var(--dur-fast) ease;
}

.details-toggle:hover {
  background-color: var(--surface-sunken);
  border-color: var(--line-strongest);
  color: var(--ink-body);
}

.details-toggle:not(.collapsed) {
  background-color: var(--brand-tint);
  border-color: var(--brand-tint-edge);
  color: var(--brand-accent-700);
}

.details-toggle .if-expanded { display: none; }
.details-toggle:not(.collapsed) .if-collapsed { display: none; }
.details-toggle:not(.collapsed) .if-expanded { display: inline; }
.details-toggle .chevron { transition: transform var(--dur-base) var(--ease-out); }
.details-toggle:not(.collapsed) .chevron { transform: rotate(180deg); }

/* Two-step search: departing -> returning progress track. */
.step-track { font-size: .875rem; }

.step-track__item {
  display: flex;
  align-items: center;
  gap: .4rem;
  color: var(--ink-secondary);
}

.step-track__item.is-current { color: var(--brand-accent); font-weight: 600; }
.step-track__item.is-done { color: var(--brand-success); }

.step-track__marker {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 1px solid currentColor;
  font-size: .75rem;
  line-height: 1;
}

.step-track__item.is-current .step-track__marker { background-color: var(--brand-accent-fill); border-color: var(--brand-accent); color: var(--on-brand); }
.step-track__item.is-done .step-track__marker { background-color: var(--brand-success-fill); border-color: var(--brand-success); color: var(--on-brand); }

/* The connector between two steps. It shrinks: at 40px fixed, three labels and
   two rules came to more than a 375px screen and pushed "Review & book" off the
   edge, taking the whole page's horizontal scroll with it. The rule is the only
   part here that can lose width without losing meaning. */
.step-track__line {
  flex: 0 1 40px;
  min-width: .5rem;
  height: 1px;
  background-color: var(--line-strongest);
}

/* Mark the option the pointer is over. This is deliberately only a shadow:
   the card is a plain div with no handler on it, and the only thing that
   advances a step is the Select button inside, so lifting the whole card
   would advertise a press that does nothing. The transition used to reserve
   `transform` for that lift -- the reservation outlived the idea.
   Hover-gated: on a touchscreen :hover latches on tap, and a single card
   left holding a raised shadow in a list of two hundred reads as chosen. */
.flight-card { transition: box-shadow var(--dur-fast) ease, opacity var(--dur-base) var(--ease-out); }

/* Twenty cards arriving in a single frame reads as the page flinching. A fade
   is enough to say "these are new" without holding up the read -- no stagger
   and no movement, because this is a list being scanned for a price, not an
   entrance worth watching. Only appended cards get it: the first ten arrive
   with the page and should be there the instant it paints. */
.flight-card.is-arriving { opacity: 0; }

@media (hover: hover) and (pointer: fine) {
  .flight-card:hover { box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .1) !important; }
}

/* The assembled round trip, set apart from the option lists. */
.trip-package { border: 2px solid var(--brand-accent); }

.trip-package__banner {
  background-color: var(--brand-accent-fill);
  color: var(--on-brand);
  padding: .5rem 1rem;
  border-radius: calc(var(--bs-border-radius) - 2px) calc(var(--bs-border-radius) - 2px) 0 0;
}

/* Route bar: a part per flight leg and per layover, each sized by its minutes.
   Hovering a part highlights just that stretch of the journey. */
.route { padding-bottom: 1rem; }

.route__head {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin-bottom: .3rem;
  color: var(--ink-secondary);
  font-size: .8125rem;
}

.route__summary { flex: 1; text-align: center; }

.route__transit { display: block; }

/* Quieter than the transit line above it: the waits are detail you read once
   you have decided the total time is acceptable. */
.route__waits {
  display: block;
  color: var(--ink-muted);
  font-size: .75rem;
}

/* The carbon estimate sits on the transit line because that is where the
   comparable numbers are -- time, stops, then this. It inherits that line's
   colour, so it is a fact among facts rather than a claim. */
.route__co2 { white-space: nowrap; }

/* Green only when the itinerary is at or below the middle of what the route
   offered. Colour is doing real work here, so the card says it in words too --
   in a visually-hidden span, because the leaf beside it is decorative and a
   tooltip is not announced. */
.route__co2--low { color: var(--brand-success); }

.route__bar { display: flex; align-items: center; gap: 4px; }

/* Every part is the same height, and the line you see is drawn by ::before.
   Two things follow, both of which were wrong when the part *was* the line: a
   2px layover is now a 16px hover target, and because the boxes are identical
   the airport codes beneath them share one baseline instead of the stop
   sitting 2px low and jumping as its bar thickened on hover. */
.route__leg,
.route__stop {
  position: relative;
  flex-basis: 0;
  cursor: pointer;
  display: flex;
  align-items: center;
  height: 16px;
}

.route__leg::before,
.route__stop::before {
  content: "";
  display: block;
  width: 100%;
  border-radius: 3px;
  transition: background-color var(--dur-fast) ease, transform var(--dur-fast) var(--ease-out);
}

/* A flight is a solid bar; a layover is the thin gap between two flights.

   The minimum is set by the label, not the bar. A code is absolutely
   positioned and so contributes nothing to sizing, which let a 26px part carry
   a 28px code that ran into its neighbour's — a short first leg printed as
   "MEL SYD" with the two touching. Every part now owns a lane wide enough for
   its own code, which costs a little proportional accuracy on the shortest
   legs and buys labels that can always be read. */
.route__leg { min-width: 2.25rem; }
.route__leg::before { height: 6px; background-color: var(--line-strongest); }

.route__stop { min-width: 2.25rem; }
/* Drawn at a leg's thickness and scaled down to a layover's, so the hover
   animates `transform` and not `height`. This bar is the most-hovered surface
   in the product -- several parts per card, up to 210 cards -- and `height`
   put a layout pass on every one of them. scaleY works from the centre and
   the parent already centres the bar, so nothing shifts; at full scale the
   stop is geometrically identical to the leg beside it. */
.route__stop::before {
  height: 6px;
  transform: scaleY(.3333);
  background-color: var(--line-strong);
}

/* Hover is a pointer affordance. On a touchscreen :hover latches on tap and
   stays latched, leaving a segment of somebody's flight highlighted for no
   reason they can undo. */
@media (hover: hover) and (pointer: fine) {
  .route__leg:hover::before { background-color: var(--brand-accent-fill); }

  .route__stop:hover::before {
    transform: scaleY(1);
    background-color: var(--brand-accent-fill);
  }
}

.route__code {
  position: absolute;
  /* Measured from the bottom edge of an equal-height part, so the departure,
     the connections and the arrival all label the bar from the same line. */
  top: 100%;
  font-size: .75rem;
  font-weight: 500;
  color: var(--bs-primary);
  white-space: nowrap;
}

.route__code--start { left: 0; }
.route__code--end { right: 0; }

.route__code--stop {
  left: 50%;
  transform: translateX(-50%);
}

/* The connection airport lights up with its own leg of the journey -- gated
   with the bar above it, so the pair stay in step on a touchscreen. */
@media (hover: hover) and (pointer: fine) {
  .route__stop:hover .route__code { color: var(--brand-accent); }
}

/* The codes sit below the bar, so they are the only thing occupying that strip
   and can safely take the pointer. Hovering a code is hovering its part: the
   tooltip opens and the bar lights up, whether you aim at the line or at the
   three letters under it. A connection code is centred on a part narrower than
   itself and is allowed to overhang without widening the bar. */
.route__code {
  padding: 2px 3px;
  margin: -2px -3px;
}

/* Badge label sitting on the shoulder of a card it describes. */
.flight-badges {
  display: flex;
  gap: .35rem;
  position: relative;
  z-index: 3;
  margin-bottom: -.65rem;
  padding-left: .75rem;
}

/* Shape only. The colour used to be white here as well, which was right while
   all four badges were saturated fills and wrong the moment two of them became
   tinted -- and wrong at a specificity the modifiers could not answer, so a
   tinted chip rendered white on pale grey. Each modifier states its own pair. */
.flight-badges .badge {
  font-size: .8125rem;
  padding: .35rem .7rem;
  border-radius: 999px;
}





.badge--secondary { background-color: var(--ink-secondary); }

/* The flight a Change link came back to replace. Deliberately the brand accent
   rather than one of the sort badges' colours: those describe how a row scored,
   this one says the traveller already has it. */
/* How a row scored, in the palette the rest of the app is in.

   These were four saturated fills -- a green, a violet, a bright blue and a
   second teal -- and three things were wrong with them. Two failed WCAG AA
   outright: white measured 3.22 on the green and 3.44 on the teal. Two were
   near-misses of tokens that already existed, a second green beside
   --brand-success and a second teal beside --brand-accent, which reads as a
   mistake rather than a choice. And the blue was #2f6fed, which is the stock
   Bootstrap blue this palette was written to purge.

   Two tiers now, which is what the four labels actually are. Cheapest and Best
   value are claims about the whole list and carry a filled chip; Fastest and
   Cheapest nonstop qualify one, and are tinted. Every pairing is measured:
   6.45, 15.60, 7.87 and 7.04 against their own backgrounds.

   No new tokens. The tinted pair is the treatment .search-cabin already uses
   for the same job a few lines up. */
.badge--success {
  background-color: var(--brand-success-fill);
  color: var(--on-brand);
}

.badge--primary {
  background-color: var(--brand-ink-chip);
  color: var(--on-brand);
}

.badge--purple {
  background-color: var(--surface-sunken);
  border: 1px solid var(--line-strong);
  color: var(--ink-secondary);
}

.badge--teal {
  background-color: var(--brand-tint);
  border: 1px solid var(--brand-tint-edge);
  color: var(--brand-accent-700);
}

.badge--current { background-color: var(--brand-accent-fill-700); }

.flight-card--badged { border-top-left-radius: .5rem; }

/* Ringed rather than filled: the row still has to be readable against its
   neighbours and comparable with them, so the marker sits on the edge instead
   of tinting the content.

   The ring is the border alone. A box-shadow here is dead on arrival -- the
   card also carries Bootstrap's .shadow-sm, and Bootstrap writes its utilities
   with !important, so the shadow cannot be overridden from a class. Widening
   the border is the honest way to make it read, rather than answering an
   !important with another one. */
.flight-card--current {
  border-color: var(--brand-accent);
  border-width: 2px;
}

/* Round icon buttons: save and share. */
.icon-btn {
  width: 38px;
  height: 38px;
  border: 0;
  border-radius: 50%;
  background-color: var(--surface-sunken);
  color: var(--ink-secondary);
  transition: background-color var(--dur-fast) ease, color var(--dur-fast) ease;
}

.icon-btn:hover { background-color: var(--line); }
.icon-btn.is-active { color: var(--brand-danger); }
.icon-btn.is-copied { background-color: var(--brand-tint); color: var(--brand-success); }

/* Removing a saved flight: the one destructive control on the saved page. */
.icon-btn--danger:hover { background-color: var(--danger-tint); color: var(--brand-danger); }

/* Dropping a saved flight used to delete the card mid-click, so everything
   below jumped up under the pointer at the exact moment it was busy. The card
   now collapses out of the way first: the fade says which one is going, the
   height says where the list is about to close up.
   The transition is restated in full because this rule sits after
   .flight-card at the same specificity and would otherwise take the hover
   shadow with it. Under reduced motion the collapse is instant and only the
   fade runs -- and the removal does not wait on either. */
.saved-item {
  transition: box-shadow var(--dur-fast) ease,
              opacity var(--dur-base) var(--ease-out),
              height var(--dur-base) var(--ease-out),
              margin-bottom var(--dur-base) var(--ease-out);
}

.saved-item.is-leaving {
  overflow: hidden;
  opacity: 0;
  margin-bottom: 0;
}

/* Empty states share one icon treatment: large enough to anchor the block,
   pale enough not to compete with the sentence explaining what goes here. */
.empty-state__icon {
  font-size: 3rem;
  color: var(--line-strongest);
}

/* A thing worth knowing before you choose: tight connection, night wait, … */
.notice {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  font-size: .875rem;
  cursor: help;
}

/* One treatment for all of them: these are things to look twice at, and the
   uniform red is the wanted look.

   It is not the ORIGINAL red, though. That was #d6336c on #FFE9E9, which
   measures 3.98:1 and misses AA. Same appearance, accessible pairing: 5.59.

   The presenter still assigns a severity and the template still emits it as a
   class, so the three tiers are one edit away if the flat treatment ever
   starts hiding a tight connection among five facts. */
.notice--risk,
.notice--check,
.notice--note {
  background-color: var(--danger-tint);
  color: var(--brand-danger);
}

/* Choosing a flight is a button, not the card. A whole-card target competed
   with everything else on the card that responds to a click — the timeline,
   the logos, save and share — and gave no visible promise of what it would do.
   It lives in the footer, on the same line as the details toggle. */
.card-select {
  flex: 0 0 auto;
  border-radius: 999px;
  padding: .5rem 1.25rem;
  font-weight: 600;
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   Expanded flight detail
   --------------------------------------------------------------------------
   A rail with a node per airport, one block per segment. Departure sits above
   arrival so the reading order is the travelling order, and the carrier block
   spans both rows to its right rather than competing as a fifth column.
   Times inherit the tabular figures set on body, so the two clock readings on
   a segment line up under each other whatever the digits. */
.legs {
  border: 1px solid var(--line);
  border-radius: var(--bs-border-radius-lg);
  padding: 1rem 1.125rem;
  margin-bottom: .75rem;
  background-color: var(--surface-raised);
}

.leg {
  display: grid;
  grid-template-columns: .875rem minmax(0, 1fr) minmax(0, 1fr);
  grid-template-areas:
    "rail depart carrier"
    "rail arrive carrier"
    "rail facts  facts";
  column-gap: .875rem;
  row-gap: .625rem;
}

/* The line runs the height of the segment; the nodes are pinned to the first
   and last text rows rather than to the rail's own ends, so they sit level
   with the times they belong to. */
.leg__rail {
  grid-area: rail;
  position: relative;
  justify-self: center;
  width: 2px;
  background-color: var(--line-strongest);
  /* Inset to the centre of the first and last time, so the line starts and
     ends at its nodes rather than at the edges of the grid row. */
  margin: .55rem 0;
}

.leg__rail::before,
.leg__rail::after {
  content: "";
  position: absolute;
  left: 50%;
  width: .5rem;
  height: .5rem;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  background-color: var(--surface-raised);
  border: 2px solid var(--brand-accent);
}

.leg__rail::before { top: 0; }
/* Hollow on arrival, filled on departure -- the same read as a route bar. */
.leg__rail::after  { top: 100%; background-color: var(--brand-accent-fill); }

.leg__stop { display: flex; flex-direction: column; min-width: 0; }
.leg__stop:first-of-type { grid-area: depart; }
.leg__stop:last-of-type  { grid-area: arrive; }

.leg__time {
  font-size: 1.0625rem;
  font-weight: 700;
  line-height: 1.2;
  color: var(--ink-body);
}

.leg__place {
  font-size: .8125rem;
  color: var(--ink-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.leg__code { color: var(--ink-muted); }

.leg__date { font-size: .75rem; color: var(--ink-muted); }
/* Landing on another day is the one thing here worth a second look. */
.leg__date--next { color: var(--brand-danger); font-weight: 600; }

.leg__carrier {
  grid-area: carrier;
  display: grid;
  grid-template-columns: 1.75rem minmax(0, 1fr);
  column-gap: .5rem;
  align-content: center;
  align-self: center;
}

.leg__logo { grid-row: 1 / span 3; align-self: start; border-radius: 50%; }
.leg__airline {
  font-size: .8125rem;
  font-weight: 600;
  color: var(--ink-body);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.leg__meta { font-size: .75rem; color: var(--ink-muted); }

/* Sits between two segments, indented past the rail so the gap in the
   journey is visible as a gap. */
/* Hugs its own text rather than stretching, so the interruption in the rail
   reads as the gap in the journey it describes. */
.leg__layover {
  display: inline-flex;
  align-items: center;
  gap: .375rem;
  /* The gap in the journey wants to read as a gap. At .5rem the chip sat tight
     against the facts row above it and the next departure below, so the break
     in the rail was the only thing separating one segment from the next. */
  margin: 1.125rem 0 1.125rem 1.625rem;
  padding: .3125rem .625rem;
  border-radius: 999px;
  background-color: var(--surface);
  border: 1px solid var(--line);
  font-size: .75rem;
  color: var(--ink-secondary);
}

/* Font Awesome's JS bundle rewrites every <i> into an <svg>, so a margin hung
   on `i` stops applying the moment that runs -- which is why the clock sat
   against the time. The container's own gap does not care what the icon
   element ends up being. */
.leg__layover i,
.leg__layover svg { color: var(--ink-muted); }

.leg__layover-note {
  padding-left: .5rem;
  margin-left: .125rem;
  border-left: 1px solid var(--line-strong);
  font-weight: 600;
}

.leg__layover-note--note { color: var(--ink-secondary); }
/* A wait too short to make comfortably is the one that changes a decision. */
.leg__layover-note--risk { color: var(--brand-danger); }

.legs__total {
  margin: .875rem 0 0;
  padding-top: .75rem;
  border-top: 1px solid var(--line);
  font-size: .8125rem;
  color: var(--ink-secondary);
}

/* Narrow: the carrier drops under its segment instead of squeezing beside it. */
@media (max-width: 575.98px) {
  .leg {
    grid-template-columns: .875rem minmax(0, 1fr);
    grid-template-areas:
      "rail depart"
      "rail arrive"
      "rail carrier";
  }
  .leg__carrier { padding-top: .25rem; }
}

/* The card's action strip: what you can do with this flight, set apart from
   the flight itself. More controls will land on this row. */
.flight-card__footer {
  background-color: var(--surface);
  border-top: 1px solid var(--line);
  padding: .75rem 1rem;
}

/* The two controls sit on one line until the card is too narrow for both. */
.flight-card__footer > .d-flex { flex-wrap: wrap; row-gap: .5rem; }

/* Actions split into two groups: red notice icons, then the buttons. */
.card-actions__divider {
  width: 1px;
  height: 24px;
  margin: 0 .75rem;
  background-color: var(--line-strong);
}

/* A flex child only shrinks below its content width once this is lifted, and
   the route bar has to give way to the button beside it. */
.min-w-0 { min-width: 0; }

/* Icon buttons hold both states and swap with a class, because FontAwesome
   rewrites <i> into <svg> and class edits from JS would not survive. */
.icon-btn .icon-done { display: none; }
.icon-btn.is-active .icon-idle,
.icon-btn.is-copied .icon-idle { display: none; }
.icon-btn.is-active .icon-done,
.icon-btn.is-copied .icon-done { display: inline-block; }

/* Airline logos: one per airline flown, overlapped when there is more than one
   so the pair reads as a single mark without stealing width from the route. */
.airline-logos {
  position: relative;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.airline-logo {
  width: 40px;
  height: 40px;
  object-fit: contain;
  background-color: var(--surface-raised);
}

.airline-logos--multi .airline-logo {
  position: absolute;
  width: 28px;
  height: 28px;
  border: 2px solid #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .06);
}

.airline-logos--multi .airline-logo:nth-child(1) { top: 0; left: 0; z-index: 3; }
.airline-logos--multi .airline-logo:nth-child(2) { bottom: 0; right: 0; z-index: 2; }
.airline-logos--multi .airline-logo:nth-child(3) { bottom: 0; left: 0; z-index: 1; }

/* The chosen outbound: a green banner rather than a green wash, so the route
   bar inside keeps the same contrast it has on every other card. */
.selected-flight { border: 1px solid var(--success-tint-edge); }

.selected-flight__banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  padding: .5rem 1rem;
  background-color: var(--brand-success-fill);
  color: var(--on-brand);
  border-radius: calc(var(--bs-border-radius) - 1px) calc(var(--bs-border-radius) - 1px) 0 0;
}

/* Divides what is already settled from what is still being chosen. */
.step-divider {
  display: flex;
  align-items: center;
  gap: .75rem;
  margin: 1.5rem 0 1rem;
  color: var(--ink-secondary);
  font-size: .875rem;
}

.step-divider::before,
.step-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background-color: var(--line-strong);
}

/* Price: the number is what people compare, so it carries the weight while the
   symbol, cents and currency code stay quiet around it. */
.price {
  display: flex;
  align-items: baseline;
  gap: .15rem;
  line-height: 1.1;
  color: var(--ink-body);
}

.price__symbol {
  font-size: 1.35rem;
  font-weight: 600;
  color: var(--ink-secondary);
}

.price__whole {
  font-size: 2.1rem;
  font-weight: 700;
  letter-spacing: -.02em;
}

.price__cents {
  font-size: 1.1rem;
  font-weight: 600;
  color: var(--ink-secondary);
  margin-left: -.15rem;
}

.price__currency {
  font-size: .75rem;
  font-weight: 500;
  color: var(--ink-muted);
  text-transform: uppercase;
  letter-spacing: .04em;
  margin-left: .2rem;
}

.price--lg .price__whole { font-size: 2.5rem; }

/* ==========================================================================
   Search sidebar
   ==========================================================================
   Built from the same tokens as the flight cards — the #f8f9fa panel, the
   pill radii, the orange hover — so the two halves of the page read as one
   design rather than two.
   ========================================================================== */

.filters .card-body { padding: .5rem 1rem 1rem; }

/* --- Results column: one block per idea, ruled apart -------------------
   The step track, the heading, the sort bar and the results were four things
   in one undifferentiated stack. A rule between them says where each ends. */
.results-block { padding-bottom: 1rem; margin-bottom: 1rem; border-bottom: 1px solid var(--line); }
.results-block--steps { padding-bottom: .875rem; margin-bottom: .875rem; }
.results-block--title { padding-bottom: .875rem; margin-bottom: .875rem; }
/* Ruled off from the results too, so the bar reads as a control over the
   list rather than the first item in it. */
.results-block--sort { padding-bottom: 1rem; margin-bottom: 1rem; }
.results-block--sort .sort-bar { margin-bottom: 0; }

/* --- Column widths -------------------------------------------------------
   The sidebar needs less than a third once sorting moved out of it; the extra
   goes to the results, where the route bar and the card footer use it.

   Only while there is a sidebar to sit beside, though. This width beats the
   grid class on the column, so on the review step — which drops the sidebar —
   it left the results at 72% and the vacated 28% simply empty. */
@media (min-width: 992px) {
  .search-sidebar { flex: 0 0 auto; width: 28%; }
  .search-results:not(.search-results--full) { flex: 0 0 auto; width: 72%; }
}

/* --- Sort bar above the results ------------------------------------------
   Three readings of the same list, so one connected control rather than three
   cards: the prices sit in a row and can be compared without moving the eye
   between boxes. The chosen one is filled dark rather than brand blue — blue
   is what a "Select this flight" button means here, and a sort is not that. */
.sort-bar {
  display: flex;
  align-items: stretch;
  margin-bottom: 1rem;
  border-radius: .75rem;
  /* White, because the page is not. This was --surface, which gave the bar its
     own light ground back when every page was white -- and became invisible the
     day the page took that same colour, leaving two of the three tabs floating
     with no surface at all while the chosen one looked like the only control
     there. A control on a grey page is a white one. */
  background-color: var(--surface-raised);
  overflow: hidden;
}

.sort-bar__tab {
  flex: 1 1 0;
  min-width: 0;
  padding: .75rem 1.125rem;
  border-right: 1px solid var(--line);
  color: var(--ink-secondary);
  text-decoration: none;
  cursor: pointer;
  /* The tab is a target, not a passage of text: without this the pointer
     turns into a caret over the label and the price. */
  user-select: none;
  transition: background-color var(--dur-fast) ease, color var(--dur-fast) ease;
}

/* --brand-tint, which is what every other hover in this file uses. It was
   --surface for a moment, and --surface is exactly the colour of the page: the
   hovered tab stopped looking hovered and started looking like a hole cut in
   the white bar. */
.sort-bar__tab:hover { background-color: var(--brand-tint); }

/* Ink, not accent. This marks which sort is active -- it is state, not a step
   forward -- and leaving the accent free for the Select buttons is what stops
   the two reading as the same kind of thing. */
.sort-bar__tab.is-current {
  background-color: var(--brand-ink-chip);
  border-right-color: var(--brand-ink-fill);
  color: var(--on-brand);
}

.sort-bar__label {
  display: block;
  font-size: .8125rem;
  line-height: 1.2;
}

/* The number is what the tab is for, so it carries the weight. */
.sort-bar__price {
  display: block;
  margin-top: .1rem;
  font-size: 1.375rem;
  font-weight: 700;
  line-height: 1.15;
  color: var(--ink-body);
}

.sort-bar__tab.is-current .sort-bar__price { color: var(--on-brand); }
.sort-bar__price--none { color: var(--line-strongest); }

.sort-bar__meta {
  display: block;
  font-size: .75rem;
  line-height: 1.3;
  color: var(--ink-muted);
}

.sort-bar__tab.is-current .sort-bar__meta { color: rgba(255, 255, 255, .7); }

/* The overflow sorts, in the same bar so there is one place to change order. */
/* Sits in the bar like another tab, so it needs the same footprint and its
   label centred rather than left-aligned against the divider. */
.sort-bar__more {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  min-width: 7rem;
  padding: 0 1rem;
}

/* Sized to its placeholder rather than its widest option, so "Sort" and the
   caret sit together and the pair is what gets centred. The open menu is laid
   out by the browser and still shows each option in full. */
.sort-bar__select {
  /* An explicit width, because `auto` on a select means "as wide as the
     longest option" — which left the caret a hundred pixels past the word. */
  width: 4.25rem;
  border: 0;
  background: none;
  text-align: left;
  text-align-last: left;
  padding: .5rem 1.15rem .5rem 0;
  font-size: .9375rem;
  font-weight: 600;
  color: var(--ink-body);
  cursor: pointer;
  user-select: none;
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3E%3Cpath d='M4.5 6.5 8 10l3.5-3.5z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right center;
  background-size: 1rem;
}

/* A quiet note that the other half of the trip is filtered too. */
.filters__other-leg {
  display: flex;
  align-items: flex-start;
  gap: .5rem;
  margin: .75rem 0 .25rem;
  padding: .625rem .75rem;
  border-radius: .5rem;
  background-color: var(--brand-tint);
  color: var(--brand-accent-700);
  font-size: .8125rem;
  line-height: 1.35;
}

.filters__other-leg i { margin-top: .1rem; }

/* --- Section: title, optional Select all, chevron ------------------------ */
.filter-section { border-bottom: 1px solid var(--surface-sunken); }
.filter-section:last-of-type { border-bottom: 0; }

.filter-section__head {
  display: flex;
  align-items: center;
  gap: .5rem;
}

/* Takes the slack so the link and the chevron are pushed to the right. */
.filter-section__toggle {
  flex: 1;
  padding: 1rem 0;
  border: 0;
  background: none;
  text-align: left;
}

.filter-section__chevron-btn {
  flex: 0 0 auto;
  padding: 1rem 0 1rem .75rem;
  border: 0;
  background: none;
  line-height: 1;
}

.filter-section__title {
  font-weight: 600;
  color: var(--ink-body);
}

.filter-section__chevron {
  color: var(--ink-muted);
  font-size: .8125rem;
  transition: transform var(--dur-base) var(--ease-out);
}

.filter-section__chevron-btn.collapsed .filter-section__chevron { transform: rotate(180deg); }

.filter-section__link {
  border: 0;
  background: none;
  padding: 0;
  color: var(--bs-primary);
  font-size: .875rem;
  white-space: nowrap;
}

.filter-section__link:hover { text-decoration: underline; }

/* Select all belongs to the controls it acts on, so it goes away with them
   rather than sitting next to a heading with nothing under it. */
.filter-section__toggle.collapsed ~ .filter-section__link { display: none; }

/* Label reflects what the button will do; JS flips the class on the section. */
.filter-section__link .if-some { display: none; }
.filter-section.has-selection .filter-section__link .if-none { display: none; }
.filter-section.has-selection .filter-section__link .if-some { display: inline; }

/* A group with nothing to "select all" shows the button only once there is
   something to clear — an always-visible dead control is worse than none. */
.filter-section__link--clear-only { display: none; }
.filter-section.has-selection .filter-section__link--clear-only { display: inline; }

.filter-section__body { padding-bottom: 1rem; }

.filter-subhead {
  font-size: .875rem;
  color: var(--ink-secondary);
  margin-bottom: .5rem;
}

/* --- Sort options -------------------------------------------------------- */
.sort-option {
  display: flex;
  align-items: stretch;
  gap: .75rem;
}

/* Spans the full height of the option — title and note both — and centres in
   it, rather than perching next to the first line. Fixed width so the labels
   line up whatever the glyph. */
.sort-option__icon {
  display: flex;
  flex: 0 0 2.25rem;
  align-items: center;
  justify-content: center;
  /* Sized to the two lines beside it, so the glyph reads as the block's mark
     rather than a bullet next to the title. */
  font-size: 1.75rem;
  line-height: 1;
  color: var(--ink-muted);
}

.sort-option__text { flex: 1; min-width: 0; }

/* The chosen option is filled, so its icon has to read against that. */
.list-group-item-check:checked + .sort-option .sort-option__icon { color: inherit; }

/* --- Rows in a checkbox list -------------------------------------------- */
.filter-list { display: flex; flex-direction: column; }

.filter-row {
  display: flex;
  align-items: center;
  gap: .625rem;
  padding: .4rem .5rem;
  margin: 0 -.5rem;
  border-radius: .5rem;
  cursor: pointer;
}

.filter-row:hover { background-color: var(--surface); }

.filter-row__logo {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  object-fit: contain;
  flex: 0 0 auto;
}

.filter-row__text { flex: 1; min-width: 0; }

/* With the control moved to the front, the price is the last thing in every
   row of every group — so all of them right-align to one column instead of
   each group ending wherever its own control happened to be wide. */
.filter-row__check,
.filter-toggle .form-check { order: -1; }

/* The price under a long name rather than beside it. */
.filter-row__price--below {
  display: block;
  margin-top: .05rem;
  font-size: .75rem;
}

/* "Only" narrows to one option. Hidden until the row is hovered or focused, so
   fourteen of them do not compete with the names for attention. */
.filter-row__only {
  flex: 0 0 auto;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background-color: var(--surface-raised);
  padding: .1rem .55rem;
  font-size: .75rem;
  color: var(--bs-primary);
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease-out), background-color var(--dur-fast) ease;
}

.filter-row:hover .filter-row__only,
.filter-row__only:focus-visible { opacity: 1; }
.filter-row__only:hover { background-color: var(--brand-tint); }

.filter-row__label {
  display: block;
  font-size: .9375rem;
  color: var(--ink-body);
}

/* The code sits with the name, quieter, the way the reference prints it. */
.filter-row__sub {
  color: var(--ink-muted);
  padding-left: .35rem;
}

.filter-row__note {
  display: block;
  font-size: .8125rem;
  color: var(--ink-muted);
}

.filter-row__check { flex: 0 0 auto; margin: 0; }

/* What picking this option would cost. Quiet, but aligned in a column so the
   list can be read down as a price list rather than option by option. */
.filter-row__price {
  flex: 0 0 auto;
  font-size: .8125rem;
  color: var(--ink-secondary);
  white-space: nowrap;
}

.filter-pill__price {
  padding-left: .35rem;
  font-size: .75rem;
  opacity: .75;
}

.filter-pill.is-on .filter-pill__price { opacity: .9; }

/* A list longer than its limit keeps the tail hidden until asked. */
.filter-row.is-over-limit,
.filter-row.is-filtered-out { display: none; }
.filter-list.is-expanded .filter-row.is-over-limit { display: flex; }
.filter-list.is-expanded .filter-row.is-filtered-out { display: none; }

.filter-more {
  border: 0;
  background: none;
  padding: .5rem 0 0;
  color: var(--bs-primary);
  font-size: .875rem;
}

.filter-more:hover { text-decoration: underline; }

/* --- Search field above a long list ------------------------------------- */
.filter-search { position: relative; margin-bottom: .5rem; }

.filter-search__icon {
  position: absolute;
  left: .75rem;
  top: 50%;
  transform: translateY(-50%);
  color: var(--ink-muted);
  font-size: .875rem;
}

.filter-search__input {
  width: 100%;
  border: 0;
  border-radius: .625rem;
  background-color: var(--surface-sunken);
  padding: .5rem .75rem .5rem 2.25rem;
  font-size: .9375rem;
}

.filter-search__input::placeholder { color: var(--ink-muted); }

/* --- Toggles ------------------------------------------------------------- */
.filter-toggle {
  display: flex;
  align-items: center;
  gap: .625rem;
  padding: .5rem 0;
  cursor: pointer;
}

/* Takes the slack, the same way a checklist row's text does, so the price and
   the switch end up together against the right edge. With space-between and no
   flexible item the three were spread evenly and the price sat in mid-row,
   detached from the control it belongs to. */
.filter-toggle__text {
  flex: 1;
  min-width: 0;
  font-size: .9375rem;
  color: var(--ink-body);
}

/* The last word and the icon move as one, so the icon can never be the only
   thing on a line. */
.filter-toggle__tail { white-space: nowrap; }

.filter-toggle__info {
  display: inline-block;
  color: var(--ink-muted);
  font-size: .8125rem;
  padding-left: .3rem;
  cursor: help;
}

/* Bootstrap reserves 2.5em of left padding on a .form-check for the label that
   normally sits inside it. There is no label here — the toggle's text is its
   own flex item — so that padding was pure gap, holding the switch 45px off the
   price instead of 10. */
.filter-toggle .form-check {
  flex: 0 0 auto;
  padding-left: 0;
  min-height: 0;
}

.filter-toggle .form-switch .form-check-input {
  width: 2.5rem;
  height: 1.4rem;
  margin: 0;
  cursor: pointer;
}

/* The colours of a switch, which were Bootstrap's and not this site's.

   Off, the track took `--bs-body-bg` and the border `--bs-border-color`, which
   in the dark palette are the panel and a hairline on it -- so the control was
   the same colour as what it sat on and only the faint knob gave it away.

   On, it was #0d6efd: stock Bootstrap blue, in both palettes. Compiled into the
   stylesheet as a literal rather than read from a variable, which is the same
   trap the nav pills had and is why a filter that was switched on has never
   been the brand's colour. */
.form-switch .form-check-input {
  --bs-form-switch-bg: var(--switch-knob);
  background-color: var(--surface-sunken);
  /* `--ink-faint` and not a line token: this is the outline of a control and
     wants the 3:1 a graphic needs, which is the bar that token is measured
     against. `--line-strongest` came out at 1.91 on a dark panel -- a hairline
     nobody would find. */
  border-color: var(--ink-faint);
}

.form-switch .form-check-input:checked {
  background-color: var(--brand-accent-fill);
  border-color: var(--brand-accent-fill);
}

/* --- Time-of-day pills ---------------------------------------------------- */
.filter-pills { display: flex; flex-wrap: wrap; gap: .4rem; }

.filter-pill {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  padding: .4rem .75rem;
  border-radius: 999px;
  background-color: var(--surface-sunken);
  color: var(--ink-body);
  font-size: .875rem;
  cursor: pointer;
  transition: background-color var(--dur-fast) ease, color var(--dur-fast) ease;
}

/* The box still has to submit, so it is hidden by size rather than by
   display:none. It needs an explicit one — an absolutely positioned input with
   no width takes its natural one and pushes the page sideways. */
.filter-pill input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  opacity: 0;
  pointer-events: none;
}
.filter-pill:hover { background-color: var(--line); }
.filter-pill__icon { color: var(--ink-muted); font-size: .8125rem; }

/* A selected pill. `is-on` is set by JS on load and on every change.
   :has(input:checked) would express this in CSS alone, but the state is then
   defined in two places that can drift. The sidebar already needs JS for its
   sliders, so one mechanism it is. */
.filter-pill.is-on {
  background-color: var(--brand-accent-fill);
  color: var(--on-brand);
}

.filter-pill.is-on .filter-pill__icon { color: var(--on-brand); }

/* --- Sliders -------------------------------------------------------------- */
.filter-slider {
  background-color: var(--surface);
  border-radius: .75rem;
  padding: .75rem .875rem 1.25rem;
}

.filter-slider__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  margin-bottom: .25rem;
}

/* The pill takes what it needs and the label gives way, so a two-ended
   caption never pushes the row into a second line. */
.filter-slider__label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: .9375rem;
  color: var(--ink-body);
}

.filter-slider__value {
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  flex: 0 0 auto;
  background-color: var(--line);
  border-radius: 999px;
  padding: .2rem .6rem;
  font-size: .875rem;
  font-weight: 600;
  color: var(--ink-body);
  white-space: nowrap;
}

/* Set, so it reads as a filter you have applied rather than a reading of the
   range on offer. */
.filter-slider__value.is-on {
  background-color: var(--brand-accent-fill);
  color: var(--on-brand);
  padding-right: .2rem;
}

.filter-slider__clear {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.125rem;
  height: 1.125rem;
  border: 0;
  border-radius: 50%;
  background-color: rgba(255, 255, 255, .3);
  padding: 0;
  font-size: .625rem;
  line-height: 1;
  color: var(--on-brand);
}
.filter-slider__clear:hover { background-color: rgba(255, 255, 255, .55); }
.filter-slider__clear[hidden] { display: none; }

/* --- Apply --------------------------------------------------------------- */
/* The menu stays put on the homepage, and only there.

   That page folds its hero into the header -- the section tray moves up into it
   and the search bar sticks beneath -- and the two have to sit together for
   that to read as one band rather than as a bar that has come loose. Nowhere
   else is anything depending on it, and a menu holding the top of every page is
   a strip of chrome taken off the content for the rest of the visit.

   Matched on the hero rather than on a class the page would have to carry: the
   hero is what makes this the page the header has a job in, and it is rendered
   nowhere else. `.hero__modes` and `.hero-cards` are their own classes and do
   not match.

   Above the sticky search bar at 1020, below Bootstrap's modal at 1055. */
body:has(.hero) header#top {
  position: sticky;
  top: 0;
  z-index: 1030;
}

/* Where the homepage's section tray lands once the slogan has gone.

   Centred on the header rather than placed between the logo and the menu: the
   tray is the page's own subject and belongs on the page's axis, and a flex
   slot between two lists of different widths would put it wherever those two
   happened to leave room. That is also why the header menu is two items -- see
   site.main-menu -- a full one leaves nothing for this to land in. */
.navbar > .container-fluid { position: relative; }

.navbar-dock {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
}

/* Only where the tray fits between the brand and the menu. It is centred on the
   header rather than laid out between them, so it does not push what it runs
   into -- it draws straight over it, and between the navbar's own breakpoint and
   this one it did: 640px of tray against a 210px menu sitting 92px off the right
   edge needs about 1276px of window, and at 1096px it covered My bookings by 77.

   Below this the tray stays in the hero and scrolls away, and on the pages that
   have no hero it is not shown. Nothing becomes unreachable: Saved is in the
   footer, Flights is the page you are on, and the rest are placeholders. */
@media (max-width: 1279.98px) {
  .navbar-dock { display: none; }
}

/* Pinned to the bottom of the screen so Apply is reachable without scrolling
   back down a long sidebar. It needs an opaque background and a shadow: the
   controls above scroll underneath it, and without those the two would blur
   together. */
/* Held against the bottom of the screen while the sidebar is in view, and
   settling into place once you reach the end of the list. Opaque with a shadow
   because the filter groups scroll underneath it. */
.filters__apply {
  position: sticky;
  bottom: 0;
  z-index: 3;
  margin: 0 -1rem -1rem;
  padding: 1rem;
  background-color: var(--surface-raised);
  border-top: 1px solid var(--line);
  border-radius: 0 0 var(--bs-card-border-radius) var(--bs-card-border-radius);
  box-shadow: 0 -.5rem 1rem -.5rem rgba(0, 0, 0, .1);
  text-align: center;
}

.filters__clear {
  display: inline-block;
  margin-top: .5rem;
  font-size: .875rem;
  color: var(--ink-muted);
}

/* --- Unavailable ---------------------------------------------------------
   One rule for every control. Availability is the whole point of the
   greying, so it must look identical wherever it appears rather than being
   restyled per control. */
.is-unavailable {
  opacity: .45;
  cursor: not-allowed;
  pointer-events: none;
}

.filter-row__none {
  display: block;
  font-size: .8125rem;
  color: var(--ink-muted);
  /* Never split across lines: "no" above "flights" reads as two thoughts. */
  white-space: nowrap;
}

/* On a toggle it sits in the price slot rather than under the label. */
.filter-row__none--inline { flex: 0 0 auto; }

/* --------------------------------------------------------------------------
   A list that grows
   -------------------------------------------------------------------------- */
.show-more {
  margin: 1.5rem 0 2.5rem;
  text-align: center;
}

/* An anchor, not a button: without scripting it loads a longer list. */
.show-more__button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  min-width: 14rem;
  border: 1px solid var(--bs-primary);
  border-radius: 999px;
  padding: .6rem 1.75rem;
  background-color: var(--surface-raised);
  color: var(--bs-primary);
  font-weight: 600;
  text-decoration: none;
  transition: background-color var(--dur-fast) ease, color var(--dur-fast) ease;
}
.show-more__button:hover {
  background-color: var(--brand-accent-fill);
  color: var(--on-brand);
}
/* Mid-request the label already says so; dimming it stops a second click
   reading as a dead button. */
.show-more__button.is-loading {
  opacity: .65;
  pointer-events: none;
}

.show-more__spinner {
  width: .85rem;
  height: .85rem;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: show-more-spin .7s linear infinite;
}
.show-more__spinner[hidden] { display: none; }

@keyframes show-more-spin {
  to { transform: rotate(360deg); }
}

/* Respect a visitor who has asked for less movement. */
@media (prefers-reduced-motion: reduce) {
  .show-more__spinner { animation: none; }
}

.show-more__note {
  margin: .6rem 0 0;
  color: var(--ink-secondary);
  font-size: .875rem;
}

/* --------------------------------------------------------------------------
   Back to top
   -------------------------------------------------------------------------- */
.to-top {
  position: fixed;
  right: 1.25rem;
  bottom: 1.25rem;
  z-index: 1030;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  background-color: var(--surface-raised);
  box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .15);
  color: var(--bs-primary);
  font-size: 1rem;
  /* Faded in by a class rather than by toggling `hidden`, so the button can
     animate out instead of vanishing under the reader's cursor. */
  opacity: 0;
  transform: translateY(.5rem);
  transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out),
              background-color var(--dur-fast) ease, color var(--dur-fast) ease;
}
.to-top.is-visible {
  opacity: 1;
  transform: none;
}
.to-top:hover {
  background-color: var(--brand-accent-fill);
  color: var(--on-brand);
}

/* Not the footer's ring: this button is fixed over the page, which is white to
   grey, so it takes the accent the rest of the light-background app uses. */
.to-top:focus-visible {
  outline: 2px solid var(--brand-accent);
  outline-offset: 2px;
}
.to-top[hidden] { display: none; }

/* Clear of the footer's content on a narrow screen, where the column stacks. */
@media (max-width: 575.98px) {
  .to-top {
    right: .75rem;
    bottom: .75rem;
  }
}

/* --------------------------------------------------------------------------
   Checkout
   -------------------------------------------------------------------------- */
.checkout { max-width: 1140px; padding-top: 2.5rem; }

.checkout__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.25rem;
}
.checkout__title { margin: 0; font-size: 1.75rem; font-weight: 700; color: var(--ink-body); }
.checkout__change { color: var(--brand-accent); font-weight: 500; text-decoration: none; }
.checkout__change:hover { text-decoration: underline; }

.checkout__alert {
  display: flex;
  align-items: center;
  gap: .5rem;
  border: 1px solid var(--danger-tint-edge);
  border-radius: .75rem;
  background-color: var(--danger-tint);
  padding: .75rem 1rem;
  margin-bottom: 1.25rem;
  color: var(--brand-danger);
}

/* Form and summary side by side; the summary drops under the form on a narrow
   screen, where a sticky column would eat the viewport. */
.checkout__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 22rem;
  gap: 1.5rem;
  align-items: start;
}
.checkout__aside { position: sticky; top: 1.5rem; }

@media (max-width: 991.98px) {
  .checkout__grid { grid-template-columns: minmax(0, 1fr); }
  .checkout__aside { position: static; }
}

.pane {
  border: 1px solid var(--line);
  border-radius: 1rem;
  background-color: var(--surface-raised);
  padding: 1.25rem 1.5rem 1.5rem;
  margin-bottom: 1.25rem;
}
.pane__heading {
  display: flex;
  align-items: center;
  gap: .625rem;
  margin: 0 0 .25rem;
  font-size: 1.0625rem;
  font-weight: 600;
  color: var(--ink-body);
}
.pane__step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  background-color: var(--brand-accent-fill);
  color: var(--on-brand);
  font-size: .8125rem;
}
.pane__hint { margin: 0 0 1rem; color: var(--ink-secondary); font-size: .875rem; }
.pane--accept { background-color: var(--surface); }

/* Two per row, one per row once there is no longer space for two. */
.field-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 1rem;
}
.field { margin-bottom: 1rem; min-width: 0; }
.field__label {
  display: block;
  margin-bottom: .3125rem;
  font-size: .875rem;
  font-weight: 500;
  color: var(--ink-secondary);
}
.field__input {
  width: 100%;
  border: 1px solid var(--line-strong);
  border-radius: .5rem;
  padding: .5rem .75rem;
  font-size: .9375rem;
  color: var(--ink-body);
  background-color: var(--surface-raised);
  transition: border-color var(--dur-fast) ease, box-shadow var(--dur-fast) ease;
}
.field__input:focus {
  outline: none;
  border-color: var(--bs-primary);
  box-shadow: 0 0 0 .2rem rgba(13, 110, 253, .15);
}
.field__input.is-invalid { border-color: var(--brand-danger); }
.field__error { margin: .3125rem 0 0; font-size: .8125rem; color: var(--brand-danger); }
.field__note { margin: .3125rem 0 0; font-size: .8125rem; color: var(--ink-muted); }

/* Fields and the card preview beside each other, the card first on a narrow
   screen so what is being filled in is visible while typing. */
.pay {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 20rem;
  gap: 1.5rem;
  align-items: start;
}
.pay__fields { min-width: 0; }
.pay__secure {
  margin: 1rem 0 0;
  color: var(--ink-secondary);
  font-size: .8125rem;
  line-height: 1.5;
}

@media (max-width: 767.98px) {
  .pay { grid-template-columns: minmax(0, 1fr); }
  .pay__preview { order: -1; }
}

/* --- the card ------------------------------------------------------------ */
.pay-card { perspective: 1000px; }
.pay-card__faces {
  position: relative;
  aspect-ratio: 1.586;            /* ISO/IEC 7810 ID-1, as a real card */
  transform-style: preserve-3d;
  transition: transform var(--dur-slow) var(--ease-in-out);
}
.pay-card.is-flipped .pay-card__faces { transform: rotateY(180deg); }

.pay-card__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  border-radius: .875rem;
  padding: 1.125rem 1.25rem;
  backface-visibility: hidden;
  /* Outside the palette on purpose. This is a picture of a physical object,
     not a piece of interface: a bank card is navy plastic with a gold chip, and
     paying it in --brand-ink and --brand-accent would make it a Trip Builder
     card rather than the visitor's. Nothing here carries text or state. */
  background: linear-gradient(135deg, #1f2c47 0%, #2b3a5c 55%, #33415f 100%);
  box-shadow: 0 .5rem 1.5rem rgba(31, 44, 71, .35);
  color: var(--on-brand);
}
.pay-card__face--back { transform: rotateY(180deg); }

.pay-card__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.pay-card__brand { font-size: .9375rem; font-weight: 700; letter-spacing: .02em; }
.pay-card__wifi { opacity: .7; transform: rotate(90deg); }

.pay-card__chip {
  width: 2.25rem;
  height: 1.6rem;
  border-radius: .25rem;
  margin: .875rem 0 .75rem;
  background: linear-gradient(135deg, #d6b25e, #f2dc9b 45%, #c9a24d);
}

.pay-card__number {
  font-size: 1.125rem;
  letter-spacing: .08em;
  /* Tabular figures, or the number shifts sideways as it is typed. */
  font-variant-numeric: tabular-nums;
}

.pay-card__bottom {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  margin-top: auto;
}
.pay-card__field { min-width: 0; }
.pay-card__field--expiry { flex: 0 0 auto; text-align: right; }
.pay-card__label {
  display: block;
  font-size: .5625rem;
  text-transform: uppercase;
  letter-spacing: .08em;
  opacity: .6;
}
.pay-card__value {
  display: block;
  font-size: .8125rem;
  font-variant-numeric: tabular-nums;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pay-card__stripe {
  height: 2.5rem;
  margin: .5rem -1.25rem 1rem;
  background-color: #14203a;
}
.pay-card__signature {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  height: 1.75rem;
  border-radius: .1875rem;
  background-color: var(--surface-sunken);
  padding: 0 .625rem;
}
.pay-card__cvv {
  color: var(--ink-body);
  font-size: .8125rem;
  letter-spacing: .12em;
}
.pay-card__back-note { margin: .75rem 0 0; font-size: .6875rem; opacity: .65; }

/* --- accept and pay ----------------------------------------------------- */
.accept { display: flex; gap: .625rem; font-size: .875rem; color: var(--ink-secondary); }

/* Sized explicitly. The form block near the top of this file sets
   `input { width: 100% }`, and on a flex item that is what flex-basis:auto
   resolves to — which drew this checkbox as tall as the pane. Every other
   checkbox here escapes it by carrying Bootstrap's .form-check-input; this one
   says what it is instead of depending on that. */
.accept__box {
  flex: 0 0 auto;
  width: 1.125rem;
  height: 1.125rem;
  margin-top: .1rem;
  accent-color: var(--brand-accent);
}
.accept--invalid { color: var(--brand-danger); }

.checkout__pay {
  display: block;
  width: 100%;
  border: 0;
  border-radius: .625rem;
  background-color: var(--brand-accent-fill);
  padding: .8125rem 1rem;
  margin-top: 1.25rem;
  color: var(--on-brand);
  font-size: 1.0625rem;
  font-weight: 600;
  text-align: center;
  text-decoration: none;
  transition: filter var(--dur-fast) ease;
}
.checkout__pay:hover { filter: brightness(.92); color: var(--on-brand); }
.checkout__pay--link { max-width: 18rem; }
.checkout__test { margin: .75rem 0 0; color: var(--ink-secondary); font-size: .8125rem; }
.checkout__test code { color: var(--ink-secondary); }

/* --- order summary ------------------------------------------------------ */
.order {
  border: 1px solid var(--line);
  border-radius: 1rem;
  background-color: var(--surface-raised);
  padding: 1.25rem 1.5rem;
  margin-bottom: 1.25rem;
}
.order__heading { margin: 0 0 1rem; font-size: 1.0625rem; font-weight: 600; }
.order__leg { padding-bottom: .875rem; margin-bottom: .875rem; border-bottom: 1px solid var(--surface-sunken); }
.order__leg-label {
  margin: 0 0 .25rem;
  font-size: .6875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--ink-muted);
}
.order__route { display: flex; align-items: center; gap: .5rem; margin: 0 0 .25rem; }
.order__code { font-size: 1.125rem; font-weight: 700; color: var(--ink-body); }
.order__arrow { color: var(--ink-muted); font-size: .8125rem; }
.order__when { margin: 0; font-size: .875rem; color: var(--ink-secondary); }
.order__detail { margin: 0; font-size: .8125rem; color: var(--ink-muted); }

.order__prices { margin: 0; }
.order__price-row { display: flex; justify-content: space-between; gap: 1rem; padding: .3125rem 0; }
.order__price-row dt { font-weight: 400; color: var(--ink-secondary); font-size: .9375rem; }
.order__price-row dd { margin: 0; color: var(--ink-body); font-size: .9375rem; }
.order__price-row--total {
  border-top: 1px solid var(--line);
  margin-top: .5rem;
  padding-top: .75rem;
}
.order__price-row--total dt,
.order__price-row--total dd { font-size: 1.125rem; font-weight: 700; color: var(--ink-body); }
.order__note { margin: .75rem 0 0; font-size: .8125rem; color: var(--ink-muted); }

/* --- rules -------------------------------------------------------------- */
.rules {
  border: 1px solid var(--line);
  border-radius: 1rem;
  background-color: var(--surface-raised);
  padding: 1.25rem 1.5rem;
}
.rules__heading { margin: 0 0 1rem; font-size: 1.0625rem; font-weight: 600; }
.rules__block { margin-bottom: 1rem; }
.rules__route {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  margin: 0 0 .5rem;
  font-size: .875rem;
  font-weight: 600;
  color: var(--ink-secondary);
}
.rules__brand {
  flex: 0 0 auto;
  border-radius: 999px;
  background-color: var(--line);
  padding: .1rem .5rem;
  font-size: .75rem;
  font-weight: 600;
  color: var(--ink-secondary);
}
.rules__list { margin: 0; padding: 0; list-style: none; }
/* In a booking's Fare row the list follows the brand name, so it needs the gap a standalone block does not. */
.rules__list--compact { margin-top: .375rem; }
.rules__item {
  display: flex;
  gap: .5rem;
  padding: .1875rem 0;
  font-size: .8125rem;
  color: var(--ink-secondary);
}
.rules__icon { flex: 0 0 .875rem; margin-top: .2rem; font-size: .6875rem; color: var(--brand-success); }
.rules__item--restricted { color: var(--ink-secondary); }
.rules__item--restricted .rules__icon { color: var(--ink-muted); }

.rules__notes {
  margin: 0;
  padding-left: 1.125rem;
  border-top: 1px solid var(--surface-sunken);
  padding-top: .875rem;
  font-size: .75rem;
  color: var(--ink-secondary);
}
.rules__notes li + li { margin-top: .5rem; }

/* --- confirmation ------------------------------------------------------- */
.done { text-align: center; padding: 1.5rem 0 2rem; }
.done__icon { font-size: 2.75rem; color: var(--brand-success); }
.done__title { margin: .875rem 0 .5rem; font-size: 1.75rem; font-weight: 700; }
.done__lead { margin: 0 auto; max-width: 32rem; color: var(--ink-secondary); }
.done__reference {
  display: inline-flex;
  flex-direction: column;
  gap: .25rem;
  border: 1px dashed var(--ink-faint);
  border-radius: .75rem;
  padding: .75rem 1.75rem;
  margin-top: 1.25rem;
}
.done__reference-label,
.booking-card__ref-label {
  margin: 0;
  font-size: .6875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--ink-muted);
}
.done__reference-code {
  font-size: 1.5rem;
  font-weight: 700;
  letter-spacing: .15em;
  color: var(--ink-body);
}

.done__panels {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
  gap: 1.25rem;
  align-items: start;
}
.done__direction,
.booking-card__label {
  margin: .75rem 0 .5rem;
  font-size: .6875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--ink-muted);
}
.done__leg { padding: .625rem 0; border-bottom: 1px solid var(--surface-sunken); }
.done__leg-route { display: flex; align-items: center; gap: .5rem; }
.done__leg-when { display: flex; gap: .5rem; font-size: .875rem; color: var(--ink-secondary); }
.done__leg-sep { color: var(--ink-muted); }
.done__leg-carrier { font-size: .8125rem; color: var(--ink-muted); }
.done__demo { margin: .75rem 0 0; font-size: .8125rem; color: var(--ink-muted); }

.checkout__gone { text-align: center; padding: 3rem 0; }
.checkout__gone-icon { font-size: 2.5rem; color: var(--ink-faint); }
.checkout__gone-note { margin: .75rem auto 1.5rem; max-width: 30rem; color: var(--ink-secondary); }
.checkout__gone .checkout__pay--link { margin-inline: auto; }

/* --------------------------------------------------------------------------
   Review & book
   -------------------------------------------------------------------------- */

/* Itineraries left, price and rules on the right — the same split checkout
   uses, so the two pages read as one flow. The rail drops under the flights
   once there is no room for two columns. */
.review {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 21rem;
  gap: 1.5rem;
  align-items: start;
}
.review__rail { position: sticky; top: 1.5rem; }

@media (max-width: 991.98px) {
  .review { grid-template-columns: minmax(0, 1fr); }
  .review__rail { position: static; }
}

.review__halves { min-width: 0; }
.review__half {
  border: 1px solid var(--line);
  border-radius: 1rem;
  background-color: var(--surface-raised);
  padding: 1.125rem 1.375rem 1.25rem;
  margin-bottom: 1.25rem;
}

.review__half-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  padding-bottom: .875rem;
  margin-bottom: .5rem;
  border-bottom: 1px solid var(--surface-sunken);
}
.review__half-label {
  margin: 0;
  font-size: .75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--bs-primary);
}
.review__half-date { margin: .125rem 0 0; font-size: .9375rem; color: var(--ink-secondary); }

.review__change {
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  padding: .25rem .85rem;
  font-size: .875rem;
  font-weight: 500;
  color: var(--ink-secondary);
  text-decoration: none;
  transition: border-color var(--dur-fast) ease, color var(--dur-fast) ease;
}
.review__change:hover { border-color: var(--brand-accent); color: var(--brand-accent); }

/* Aircraft and seat, under the carrier on a timeline leg.
   Quiet by construction: these are facts somebody checks when they care, not
   claims the card is making, so they sit at the size of the meta line above
   them and in the same muted ink. */
.leg__facts {
  /* Its own row across the width of the segment rather than inside the carrier
     column: that column is half of a 1fr, which broke "A330-900neo" over three
     lines. These read as a short list, so they need the measure. */
  grid-area: facts;
  display: flex;
  flex-wrap: wrap;
  gap: .25rem 1.25rem;
  margin: .125rem 0 0;
  padding: .625rem 0 0;
  border-top: 1px dashed var(--line);
  list-style: none;
}

.leg__fact {
  display: flex;
  align-items: baseline;
  gap: .45rem;
  font-size: .8125rem;
  line-height: 1.5;
  color: var(--ink-secondary);
}

.leg__fact-icon {
  flex: none;
  width: .9rem;
  text-align: center;
  color: var(--ink-faint);
  font-size: .75rem;
}

.leg__fact-note { color: var(--ink-muted); }

/* The package's flight detail is always open, so it is separated by space
   rather than by the button that used to reveal it. */
.review__detail {
  margin-top: .875rem;
  padding-top: .875rem;
  border-top: 1px solid var(--surface-sunken);
}

/* Searchable select. The native <select> stays in the DOM because it is what
   submits, so it is taken out of the layout rather than out of the document --
   display:none would still submit, but this keeps it findable for scripts and
   honest about why it is there. */
.combo { position: relative; }

.combo__native {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.combo__list {
  position: absolute;
  z-index: 20;
  top: calc(100% + .25rem);
  left: 0;
  right: 0;
  /* Ten-ish rows: enough to scan, short enough that the field below is still
     on screen while the list is open. */
  max-height: 17rem;
  overflow-y: auto;
  margin: 0;
  padding: .25rem;
  list-style: none;
  border: 1px solid var(--line-strong);
  border-radius: .5rem;
  background-color: var(--surface-raised);
  box-shadow: 0 .5rem 1.5rem -.5rem rgba(16, 36, 63, .25);
}

.combo__option {
  padding: .4rem .6rem;
  border-radius: .375rem;
  font-size: .9375rem;
  color: var(--ink-body);
  cursor: pointer;
}

.combo__option:hover,
.combo__option.is-active {
  background-color: var(--brand-tint);
  color: var(--brand-accent-700);
}

/* The one already chosen, so reopening the list shows where you are. */
.combo__option[aria-selected="true"] {
  font-weight: 600;
  color: var(--brand-accent-700);
}

/* In the search bar a field is a fifth of the row, which is narrower than an
   airport name. The list is allowed to be wider than the field it hangs off
   rather than wrapping "London Gatwick Airport" over three lines. */
.searchbar__field .combo__list {
  right: auto;
  min-width: 24rem;
  max-width: 28rem;
  /* Taller than the checkout's country list: this one opens with two sections
     in it, and the airports should not start already scrolled. */
  max-height: 26rem;
  padding: 0 0 .25rem;
  border-radius: 1rem;
  box-shadow: 0 1rem 2.5rem rgba(21, 26, 33, .16);
}

/* Except where the field is already full width, and a wider panel would run off
   the screen. */
@media (max-width: 991.98px) {
  .searchbar__field .combo__list { right: 0; min-width: 0; max-width: none; }
}

/* A section heading inside the list -- "You searched earlier", "Cities and
   airports". Bold and dark rather than a small grey caption: the panel is a
   place with two parts, and the parts should read as parts. */
.combo__group {
  padding: .75rem .75rem .375rem;
  font-size: 1rem;
  font-weight: 700;
  color: var(--ink-body);
}

.combo__group:first-child { padding-top: .5rem; }

/* Past searches, laid across the panel as chips. A chip is one thing to take or
   leave, and wrapping them keeps the airports -- what the field is for -- from
   being pushed off the bottom. */
.combo__pills {
  display: flex;
  flex-wrap: wrap;
  gap: .375rem;
  padding: 0 .75rem .5rem;
}

.combo__pill {
  display: inline-flex;
  gap: .375rem;
  align-items: center;
  max-width: 100%;
  padding: .4375rem .75rem;
  overflow: hidden;
  font-size: .8125rem;
  color: var(--ink-body);
  white-space: nowrap;
  text-overflow: ellipsis;
  background-color: var(--surface);
  border-radius: 999px;
  cursor: pointer;
  transition: background-color var(--dur-fast) ease;
}

.combo__pill i { flex: 0 0 auto; font-size: .75rem; color: var(--ink-muted); }

/* The rest of the history, one click away. Outlined rather than filled, because
   it is the one pill on the row that chooses nothing -- everything beside it
   runs a search. */
.combo__pill--more {
  gap: .3125rem;
  font-weight: 600;
  color: var(--ink-secondary);
  background-color: transparent;
  border: 1px solid var(--line-strong);
}

.combo__pill--more:hover {
  color: var(--ink-body);
  background-color: var(--surface);
}

.combo__pill--more i { font-size: .625rem; }
.combo__pill span { overflow: hidden; text-overflow: ellipsis; }
.combo__pill:hover,
.combo__pill.is-active { background-color: var(--surface-sunken); }

/* A place: an icon, the name with its code, and the city and country under it.
   The icon says which kind of row it is -- a city that stands for several
   airports, or one airport. */
.combo__option--stacked {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 0 .625rem;
  align-items: baseline;
  padding: .5rem .75rem;
}

/* Ruled between rows the way the reference is, so a long list reads as a list
   rather than as a wall. Not after the last one, and not under a city -- a city
   and its airports are one group. */
.combo__option--stacked + .combo__option--stacked { box-shadow: inset 0 1px 0 var(--line); }
.combo__option--city + .combo__option--stacked { box-shadow: none; }

/* An airport shown under its own city, indented so the group reads as one.
   Set to the icon column and its gap, so a child's icon lands where the city's
   *name* starts -- a relationship rather than a number picked by eye, and the
   same way the help hub indents a group's list under its heading.

   These rows are one line: the second line said where the airport is, and the
   city a line above has just said it. JS drops it and this only has to place
   what is left. */
.combo__option--child {
  --combo-indent: calc(1rem + .625rem);
  padding-left: calc(.75rem + var(--combo-indent));
}

/* Masked SVG rather than a Font Awesome <i>. Its script rewrites every <i> it
   finds into an <svg>, and scanning a few hundred rows blocked the main thread
   for most of two seconds -- the dropdown took seconds to open. A mask keeps
   currentColor working, so these still take their colour from the row. */
.combo__icon {
  grid-row: 1 / span 2;
  align-self: center;
  width: 1rem;
  height: 1rem;
  background-color: var(--ink-muted);
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-size: contain;
          mask-size: contain;
}

.combo__icon--airport {
  -webkit-mask-image: var(--icon-plane);
          mask-image: var(--icon-plane);
}

.combo__icon--city {
  -webkit-mask-image: var(--icon-pin);
          mask-image: var(--icon-pin);
}

.combo__icon--recent {
  flex: 0 0 auto;
  width: .875rem;
  height: .875rem;
  -webkit-mask-image: var(--icon-clock);
          mask-image: var(--icon-clock);
}

.combo__name { grid-column: 2; font-size: .9375rem; color: var(--ink-body); }
.combo__code { grid-column: 3; grid-row: 1; font-size: .8125rem; color: var(--ink-muted); }
.combo__sub  { grid-column: 2 / -1; font-size: .8125rem; color: var(--ink-muted); }
.combo__option--city .combo__name { font-weight: 600; }

/* What was left out, said plainly rather than by a list that just stops. */
.combo__more {
  padding: .5rem .75rem .625rem;
  font-size: .75rem;
  color: var(--ink-muted);
}

.combo__empty {
  padding: .5rem .6rem;
  font-size: .875rem;
  color: var(--ink-muted);
}

/* The test-data bar above the checkout form. Deliberately not styled like the
   form's own controls: it is scaffolding for trying the flow, not a step in
   it, so it reads as an aside rather than as the first thing to fill in. */
.checkout__autofill {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem .875rem;
  margin-bottom: 1.25rem;
  padding: .75rem .875rem;
  border: 1px dashed var(--line-strong);
  border-radius: .5rem;
  background-color: var(--surface);
}

.checkout__autofill-button {
  display: inline-flex;
  align-items: center;
  padding: .35rem .85rem;
  border: 1px solid var(--brand-accent);
  border-radius: 999px;
  background-color: var(--surface-raised);
  color: var(--brand-accent-700);
  font-size: .875rem;
  font-weight: 600;
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}

.checkout__autofill-button:hover,
.checkout__autofill-button:focus-visible {
  background-color: var(--brand-accent-fill);
  color: var(--on-brand);
}

.checkout__autofill-button:active { transform: scale(.97); }

.checkout__autofill-note {
  flex: 1 1 16rem;
  color: var(--ink-muted);
  font-size: .8125rem;
  line-height: 1.5;
}

.checkout__autofill-note code {
  padding: .05rem .3rem;
  border-radius: .25rem;
  background-color: var(--surface-sunken);
  color: var(--ink-secondary);
  font-size: .9em;
}

.review__total { margin: 0 0 .875rem; }
.rules__more { margin: .875rem 0 0; font-size: .75rem; color: var(--ink-muted); }

/* --------------------------------------------------------------------------
   Press feedback
   --------------------------------------------------------------------------
   Before this block the file had exactly one :active rule in 2,459 lines, and
   it only changed a colour -- so every button in the product did nothing at
   all on the way down. A press wants answering while the finger is still on
   it; waiting for the click to navigate or submit is what makes a control
   feel disconnected. Kept deliberately small: 0.97 and 120ms reads as the
   control responding, not as an animation, which is what the filter controls
   need since they are pressed dozens of times in one refinement pass.
   ease-out rather than the file's usual bare `ease` so the movement is
   quickest at the moment of contact.
   The transition lives on :active, so the press eases down and the release
   snaps back -- none of these controls carry `transform` in their resting
   transition, Bootstrap's .btn included. That asymmetry is wanted: the press
   is the user's deliberate half, the release is the system getting out of
   the way.
   Reduced motion is handled globally, not here. */
.btn:active,
.checkout__pay:active,
.icon-btn:active,
.details-toggle:active,
.show-more__button:active,
.sort-bar__tab:active,
.filter-pill:active,
.filter-more:active,
.filter-row__only:active,
.filter-slider__clear:active,
.filter-section__toggle:active,
.filter-section__chevron-btn:active,
.review__change:active,
.article__verdict-button:active,
.to-top:active {
  transform: scale(.97);
  transition: transform var(--dur-fast) var(--ease-out);
}

/* ==========================================================================
   #BOOKING CARD
   ==========================================================================
   A booking is the same object as a search result and wears the same shell.
   Only the few things that differ live here; everything else is .flight-card,
   .route__*, .leg__*, .price and .icon-btn, unchanged. */

/* Echoes .done__reference-code one size down: on the confirmation page the
   reference is the whole screen, here it titles one card among several. The
   letter-spacing is the typographic half of an alphabet that already excludes
   O/0 and I/1 -- a reference has to survive being read aloud. */
.booking-card__ref-code {
  margin: 0;
  font-size: 1.125rem;
  font-weight: 700;
  letter-spacing: .12em;
  color: var(--ink-body);
}

/* Where a booking stands. Bootstrap's .badge is 11px and sits under the
   reference it is meant to qualify; at a glance the one word that answers
   "is this trip still on?" was the smallest thing on the card. Sized to read
   at arm's length, and coloured as a tint carrying dark text rather than a
   solid fill, which stays legible at this weight without shouting.

   Contrast, measured in the browser on the tint each sits on:
     confirmed  --brand-success on --success-tint    5.59
     cancelled  --brand-danger  on --danger-tint     5.56
     flown      --ink-secondary on --surface-sunken  7.87 */
.booking-status {
  display: inline-flex;
  align-items: center;
  padding: .3125rem .75rem;
  border-radius: 999px;
  font-size: .8125rem;
  font-weight: 600;
  line-height: 1.3;
  white-space: nowrap;
}
.booking-status--confirmed { background-color: var(--success-tint); color: var(--brand-success); }
.booking-status--cancelled { background-color: var(--danger-tint); color: var(--brand-danger); }
.booking-status--flown { background-color: var(--surface-sunken); color: var(--ink-secondary); }

/* "Departs in 40 days". Quiet beside the status pill: the status answers
   whether the trip is on, this only says how near it is. */
.booking-when {
  display: inline-flex;
  align-items: center;
  gap: .375rem;
  margin-right: .625rem;
  font-size: .8125rem;
  color: var(--ink-muted);
  white-space: nowrap;
}

/* A 38px circle beside an 18px code reads as the louder of the two. */
.icon-btn--sm { width: 30px; height: 30px; font-size: .8125rem; }

/* Mirrors .price--lg. A booking's total is a record, not an offer, and should
   not out-shout the reference above it. */
.price--sm .price__whole { font-size: 1.5rem; }

/* A flown or cancelled booking keeps every bit of its text contrast -- it is
   still a receipt somebody may need -- and loses only the lift. .shadow-sm is
   a Bootstrap utility and carries !important, so the honest place to drop it is
   the template, which simply does not emit it. */
/* Sunken rather than merely un-lifted. This was --surface, which read as a
   dimmer card while the page was white and read as nothing at all once the page
   became that colour -- the card disappeared into the ground instead of sitting
   quietly on it. A step below the page says "already flown" more clearly than
   an absent shadow did anyway. */
.booking-card--past,
.booking-card--cancelled { background-color: var(--surface-sunken); }
.booking-card--past .flight-card__footer,
.booking-card--cancelled .flight-card__footer { background-color: var(--line); }

/* What you can do with a booking, as one row. Wraps rather than squeezing, so
   the next action added here does not have to find its own corner of the page. */
.booking-actions { display: flex; flex-wrap: wrap; gap: .5rem; }

/* The rebook dialog's calendar is rendered in here and positioned against it;
   without a containing block it would land at the dialog's top-left corner. */
.modal-content { position: relative; }


/* Groups on the bookings list. They were separated by the card margin alone,
   which is the same gap that sits between two cards inside one group -- so the
   groups did not read as groups. */
.bookings section { margin-bottom: 2.75rem; }
.bookings section:last-of-type { margin-bottom: 0; }

/* How many are in this group. Bootstrap's .badge is 11px grey-on-grey and was
   losing to the heading beside it; this is the tab strip's count chip, which
   is the same thing being said in the same place. */
.bookings-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.625rem;
  height: 1.625rem;
  padding: 0 .5rem;
  border-radius: 999px;
  background-color: var(--brand-tint);
  color: var(--brand-accent-700);
  font-size: .8125rem;
  font-weight: 700;
  line-height: 1;
}

/* Two addresses drawn as one strip. Underlined-when-current rather than a
   filled pill, so it reads as "where am I in this page" and not as a pair of
   buttons competing with the actions below it. */
.bookings-tabs {
  display: flex;
  gap: .25rem;
  margin-bottom: 1.75rem;
  border-bottom: 1px solid var(--line);
}
.bookings-tabs__tab {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .625rem .875rem;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  color: var(--ink-muted);
  font-weight: 600;
  text-decoration: none;
}
.bookings-tabs__tab:hover { color: var(--ink-body); background-color: var(--surface); }
.bookings-tabs__tab.is-current { color: var(--brand-accent-700); border-bottom-color: var(--brand-accent); }
.bookings-tabs__count {
  min-width: 1.5rem;
  padding: 0 .375rem;
  border-radius: 999px;
  background-color: var(--surface-sunken);
  color: var(--ink-secondary);
  font-size: .75rem;
  font-weight: 600;
  text-align: center;
}
.bookings-tabs__tab.is-current .bookings-tabs__count {
  background-color: var(--brand-tint);
  color: var(--brand-accent-700);
}

/* A cancelled booking is stamped, not badged and not banded.

   A tint the size of the word "Confirmed" was the quietest thing on a card
   whose every other line it invalidates. A bar of solid red across the top
   fixed that and overcorrected: it shouted before the reader had reached
   anything to shout about, and three of them down a page was all the page was.

   A stamp is what a travel document actually carries when it stops being
   valid -- outlined, off-square, sitting on top of the thing it voids rather
   than replacing it. It takes the badge's place, so nothing moves.

   #B02A37 on white measures 6.50, and the outline is the same colour at full
   strength rather than a tint, so the text clears AA at this size. */
.booking-stamp {
  display: inline-flex;
  align-items: center;
  padding: .25rem .625rem;
  border: 2px solid var(--brand-danger);
  border-radius: .25rem;
  color: var(--brand-danger);
  font-size: .8125rem;
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: .12em;
  text-transform: uppercase;
  white-space: nowrap;
  /* Clockwise, sitting at the card's top right. Anticlockwise there dips the
     outer end toward the card edge and reads as a badge set down crookedly;
     turned the other way the stamp hinges off the reference it voids and the
     outer end lifts clear. */
  transform: rotate(3deg);
  /* The double rule is the stamp's whole tell; without it this is a red
     button. Drawn as a shadow so it costs no extra element. */
  box-shadow: 0 0 0 1px #FFFFFF, 0 0 0 3px rgba(176, 42, 55, .28);
}
.booking-stamp--lg {
  padding: .375rem 1rem;
  font-size: 1rem;
  letter-spacing: .16em;
  transform: rotate(4deg);
}

/* The card keeps every bit of its text contrast -- it is still a receipt
   somebody may need -- and loses the colour that says "live": the carrier
   marks go grey, which is the difference you see before you have read a word. */
.booking-card--cancelled .airline-logo { filter: grayscale(1); opacity: .65; }

/* The sentence under the stamp on the booking's own page. It explains what the
   stamp means once; the stamp does not repeat it on every block below. */
.booking-note {
  display: flex;
  align-items: flex-start;
  gap: .5rem;
  margin: 1.5rem 0 0;
  padding: .75rem 1rem;
  border: 1px solid var(--danger-tint-edge);
  border-left: 3px solid var(--brand-danger);
  border-radius: .5rem;
  background-color: var(--danger-tint);
  color: var(--ink-secondary);
  font-size: .9375rem;
}
.booking-note i { color: var(--brand-danger); margin-top: .1875rem; }

/* On its way to the cancelled page. Opacity and a collapse, both cheap to
   composite; `ease-out` because the card is leaving and the movement should be
   over before it is studied. Anyone who has asked for less motion gets the
   removal with none of it. */
.booking-card--leaving {
  opacity: 0;
  transform: scale(.98);
  transition: opacity .35s ease-out, transform .35s ease-out;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .booking-card--leaving { transition: none; }
}

/* The reference, the name on it, and the date it was paid for -- what somebody
   opens this page already holding. It used to be set at the same size as a
   card in a list of twenty, on a page showing exactly one. */
.booking-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 1.5rem;
  flex-wrap: wrap;
  padding: 1.75rem 0 1.5rem;
}
.booking-head__label {
  margin: 0 0 .25rem;
  font-size: .6875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--ink-muted);
}
.booking-head__code-row { display: flex; align-items: center; gap: .5rem; }
.booking-head__code {
  margin: 0;
  font-size: 2.25rem;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: .16em;
  color: var(--ink-body);
}
/* No code to space out -- this is a sentence, not an identifier. */
.booking-head__code--none { font-size: 1.75rem; letter-spacing: normal; }
.booking-head__who {
  margin: .625rem 0 0;
  font-size: 1.125rem;
  font-weight: 600;
  color: var(--ink-body);
}
.booking-head__meta { margin: .125rem 0 0; font-size: .875rem; color: var(--ink-muted); }
.booking-head__state { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
/* The banner above already carries the news in white on red; repeating it at
   full strength here would be the third time in 200px. */
.booking-head--cancelled .booking-head__code { color: var(--ink-secondary); }

@media (max-width: 575.98px) {
  .booking-head__code { font-size: 1.75rem; }
  .booking-head__state { order: -1; }
}

/* The footer is a dark band, and content ending a few pixels above it reads as
   having been cut off rather than finished. */
.bookings { padding-bottom: 4rem; }

/* A block of the booking page.

   The page is a stack of these and nothing else, so a new one -- baggage,
   seats, check-in -- is a template plus a line in the list at the top of
   detail.html.twig. The heading sits outside the panel so the stack can be
   read by its headings alone. */
/* Blocks sit in a grid so two short ones can share a row rather than each
   taking the full width for four lines. */
.bkblocks {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 1.25rem;
  align-items: start;
}
.bkblock { grid-column: 1 / -1; margin-bottom: 1.5rem; }
.bkblock--half { grid-column: span 1; }

/* These were 13px uppercase grey -- the same treatment as the "DEPARTING"
   label inside a card, which made the name of a whole section look like an
   annotation on the thing above it. A section heading is set as a heading:
   sentence case, dark, and larger than the body it introduces. */
.bkblock__title {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin: 0 0 .75rem;
  font-size: 1.125rem;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -.01em;
  color: var(--ink-body);
}
.bkblock__icon { color: var(--brand-accent); font-size: 1rem; }

@media (max-width: 767.98px) {
  .bkblocks { grid-template-columns: minmax(0, 1fr); }
  .bkblock--half { grid-column: 1 / -1; }
}
.bkblock__body {
  padding: 1.25rem;
  border: 1px solid var(--line);
  border-radius: .75rem;
  background-color: var(--surface-raised);
}

/* Two directions in one block, told apart by a rule rather than by a second
   panel: they are one journey. */
.bkflights__half--next { margin-top: 1.75rem; padding-top: 1.75rem; border-top: 1px solid var(--line); }

/* The summary line and the legs it summarises were touching, so the route bar
   read as the top edge of the panel below it rather than as its own line. */
.bkflights__half .legs { margin-top: 1rem; }

/* Who is on the booking. One row each, name first: a manifest is scanned down
   its left edge. */
.people { margin: 0; padding: 0; list-style: none; }
.people__row {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  padding: .5rem 0;
  border-bottom: 1px solid var(--line);
}
.people__row:last-child { border-bottom: 0; padding-bottom: 0; }
.people__name { font-weight: 600; color: var(--ink-body); }
.people__type { font-size: .8125rem; color: var(--ink-muted); }

/* Label and value, both left-aligned. Used by contact and by the receipt's
   payment lines -- anywhere a short label introduces a short value. */
.facts { margin: 0; }
.facts__row {
  display: grid;
  grid-template-columns: minmax(5rem, 7rem) 1fr;
  gap: .25rem 1rem;
  padding: .5rem 0;
  border-bottom: 1px solid var(--line);
}
.facts__row:last-child { border-bottom: 0; padding-bottom: 0; }
.facts__row dt { font-size: .8125rem; font-weight: 600; color: var(--ink-muted); }
.facts__row dd { margin: 0; color: var(--ink-body); font-size: .9375rem; word-break: break-word; }

/* What the ticket allows. */
.fare__name { margin: 0 0 .5rem; font-size: 1rem; font-weight: 600; color: var(--ink-body); }
.fare__rules { margin: 0; padding: 0; list-style: none; }
.fare__rules .rules__item { break-inside: avoid; }

/* Whether a direction is behind you. Quiet: it qualifies the heading it sits
   beside rather than competing with the booking's own status. */
.leg-state {
  display: inline-flex;
  align-items: center;
  gap: .3125rem;
  padding: .125rem .5rem;
  border-radius: 999px;
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: none;
}
.leg-state--flown { background-color: var(--surface-sunken); color: var(--ink-secondary); }
.leg-state--ahead { background-color: var(--brand-tint); color: var(--brand-accent-700); }
.bkflights__label { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }

/* Amounts, right-aligned: a column of figures is compared by its last digit. */
.receipt__amounts { margin: 0; }
.receipt__amount { display: flex; justify-content: space-between; gap: 1.5rem; padding: .3125rem 0; }
.receipt__amount dt { font-weight: 400; color: var(--ink-secondary); font-size: .9375rem; }
.receipt__amount dd { margin: 0; color: var(--ink-body); font-size: .9375rem; font-variant-numeric: tabular-nums; }
.receipt__amount--total { margin-top: .5rem; padding-top: .75rem; border-top: 1px solid var(--line-strong); }
.receipt__amount--total dt,
.receipt__amount--total dd { font-size: 1.25rem; font-weight: 700; color: var(--ink-body); }
.receipt__currency { font-size: .8125rem; font-weight: 600; color: var(--ink-muted); }
.receipt__method { margin-top: 1rem; padding-top: .75rem; border-top: 1px solid var(--line); }
.receipt__no-price { margin: 0; font-size: .875rem; color: var(--ink-muted); }

/* Inside a week, the countdown is the reason the card is being looked at. */
.booking-when--soon { color: var(--brand-accent-700); font-weight: 600; }

@media (max-width: 575.98px) {
  .facts__row { grid-template-columns: 1fr; gap: .125rem; }
}


/* ==========================================================================
   #BREADCRUMB
   ==========================================================================
   Where you are, said quietly. It sits between the nav and the page's own
   heading, and it is the only thing on a child page that says so: the header's
   active test is exact string equality, so no nav item lights up on
   /my/bookings/100001.

   Sized and coloured as one of this file's micro-labels (see .done__direction
   and .booking-card__label) rather than as navigation, because it is
   orientation and the h1 under it should stay the loudest thing on the page.
   Not uppercased though -- these are page names, and a booking reference read
   in small caps is harder to check against a phone screen than it looks. */
.breadcrumbs {
  padding-top: 1.25rem;
  font-size: .8125rem;
}

.breadcrumbs__list {
  display: flex;
  /* Wraps rather than scrolls. Three crumbs and two separators overflowed a
     375px screen once already on the step track above the search results, and
     took the whole page's horizontal scroll with them. */
  flex-wrap: wrap;
  align-items: center;
  gap: .0625rem .5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* An article centres its sheet in the container, so the trail has to centre
   with it: left where every other page puts it, it sat 148px to the left of the
   page it describes. Done on the list rather than by narrowing the container,
   because the container's own max-width changes at four breakpoints and a flat
   override would only line up at the widest of them. Below the measure both
   simply fill the space, and they stay aligned. */
body:has(main.article) .breadcrumbs__list {
  max-width: var(--content-column);
  margin-inline: auto;
}

.breadcrumbs__item {
  display: flex;
  align-items: center;
  gap: .5rem;
  color: var(--ink-muted);
}

/* Drawn here rather than as an <i>: FontAwesome's JS rewrites every <i> into
   an <svg>, so a rule selecting the icon element stops applying once it runs. */
.breadcrumbs__item + .breadcrumbs__item::before {
  content: '\203A';
  /* --ink-faint, and not the rule colour this used to take. --line-strongest
     measures 1.46 against --surface, which is the same defect the on-dark
     separators were fixed for in PR #76 and almost exactly the same number:
     that rule's note says --line-on-dark is 1.44 on the band and "disappears
     as a glyph". Both sides of the trail were drawing a separator in a colour
     mixed for hairlines between panels, and only one side was corrected.

     --ink-faint is the palette's ornament tier and measures 3.04 here, which
     clears the 3:1 a non-text mark needs. Not --ink-muted, which is what the
     crumbs themselves take and what the on-dark rule chose: on that ground
     the separators are load bearing, because the trail stops before the page
     and the last one points into the heading. Here the trail ends at the page
     it is on, so the chevron is ornament -- it should be visible and it should
     not compete with the words. The whisper was the intent; being inaudible
     was the bug.

     The same call the datepicker's today marker already makes, in the same
     words: the rule tier is for hairlines between panels, and a mark carrying
     meaning has to clear 3:1 before it is worth drawing at all. */
  color: var(--ink-faint);
  /* The glyph carries no meaning a screen reader needs -- the <ol> already
     says these are ordered steps -- and generated content is not announced. */
}

.breadcrumbs__link {
  color: var(--ink-muted);
  text-decoration: none;
  transition: color var(--dur-fast) ease;
}

/* Gated, because on a touchscreen :hover latches on tap and the crumb would
   stay dark after the page it points at has been left. */
@media (hover: hover) and (pointer: fine) {
  .breadcrumbs__link:hover { color: var(--brand-accent); text-decoration: underline; }
}

/* The page you are on is not a link, and reads a step darker so the trail has
   an end rather than trailing off. */
/* The trail inside an article's band. Same markup, a ground it was not mixed
   for -- so the colours come from the palette's on-dark set, which exists with
   its ratios measured against --brand-ink for exactly this case. */
.breadcrumbs--on-dark .breadcrumbs__list {
  justify-content: center;
}

.breadcrumbs--on-dark .breadcrumbs__item,
.breadcrumbs--on-dark .breadcrumbs__link {
  color: var(--ink-on-dark);
}

/* The same weight as the crumbs themselves, not the rule-coloured whisper the
   light trail uses. --line-on-dark is 1.44:1 on this ground, which is a hairline
   between panels and disappears as a glyph -- and here the separators are load
   bearing: the trail stops before the page, so the last one is what points into
   the heading. --ink-on-dark is 7.52:1 and makes "Home > Help >" read as one
   piece of signposting rather than two words that happen to be near each
   other. */
.breadcrumbs--on-dark .breadcrumbs__item + .breadcrumbs__item::before,
.breadcrumbs--on-dark.breadcrumbs--to-heading .breadcrumbs__item:last-child::after {
  color: var(--ink-on-dark);
}

/* A separator after the last crumb, where the trail stops one step short of
   the page because the heading below states it. Without this the path reads
   as ending at its parent rather than continuing into the title. */
.breadcrumbs--to-heading .breadcrumbs__item:last-child::after {
  content: '\203A';
  margin-left: .5rem;
  /* Colour comes from the on-dark rule above, which this shares. */
}

.breadcrumbs--on-dark .breadcrumbs__item--current {
  color: var(--ink-on-dark-lead);
}

@media (hover: hover) and (pointer: fine) {
  .breadcrumbs--on-dark .breadcrumbs__link:hover {
    color: var(--on-brand);
  }
}

.breadcrumbs__item--current {
  color: var(--ink-secondary);
  font-weight: 600;
  /* A long booking reference or city pair should not push the row wider than
     the viewport. */
  min-width: 0;
  overflow-wrap: anywhere;
}

/* ==========================================================================
   #FOOTER
   ==========================================================================
   Colour for this block lives up in #FOOTER COLOUR with the tokens it depends
   on; this is the structure.

   Grid rather than the Bootstrap row it replaced. Six columns do not divide
   into twelve, so the old markup would have needed `col-2` and an offset to
   fake it, and every breakpoint would have been a second set of classes on
   every column. Here the column count is one line per breakpoint and the
   columns themselves carry no layout at all. */

/* The app's own navy, not Bootstrap's #212529 that `bg-dark` was painting here.
   That was the last stock Bootstrap colour filling a full-width band, and it
   left a seam against the navy `html` background behind it. Same colour at both
   ends of the document now. The two on-dark inks were measured against #212529
   and are very slightly better here -- 7.52 and 11.98 against 7.43 and 11.85 --
   because navy carries less luminance than grey at the same darkness. */
.footer {
  background-color: var(--brand-ink-fill);
}

.footer__inner {
  padding-top: 3rem;
  padding-bottom: 2rem;
}

/* A band is separated from the one below it by a rule, so the last one has
   none -- the footer's own bottom edge already ends it. */
.footer__band {
  padding-bottom: 2.5rem;
  margin-bottom: 2.5rem;
  border-bottom: 1px solid var(--line-on-dark);
}

.footer__title {
  margin: 0 0 1rem;
  color: var(--ink-on-dark-lead);
  font-size: 1rem;
  font-weight: 600;
}

/* The heading over a whole band, rather than over one column of one. */
.footer__title--band {
  margin-bottom: 1.5rem;
  font-size: 1.125rem;
}

.footer__list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.footer__item + .footer__item { margin-top: .625rem; }

.footer__link {
  color: var(--ink-on-dark);
  font-size: .9375rem;
  text-decoration: none;
  transition: color var(--dur-fast) ease;
}

/* Gated for the same reason as the breadcrumbs: on a touchscreen :hover latches
   on tap, and a footer link would stay lit after the page it points at has been
   left. */
@media (hover: hover) and (pointer: fine) {
  .footer__link:hover { color: var(--ink-on-dark-lead); text-decoration: underline; }
}

/* The way out of a column, so it reads as an action rather than as a sixth
   sibling of the five above it. */
.footer__more { color: var(--ink-on-dark-lead); font-weight: 500; }

/* ---------- The six columns ---------- */

.footer__columns {
  display: grid;
  /* The columns make their own tracks rather than filling six reserved ones.
     Every column here is counted from a table, so any of them can be empty on a
     database nobody has filled yet -- a new deployment, a restored backup, a
     db:clear -- and links.html.twig drops the ones that are. A fixed
     `repeat(6, 1fr)` kept the slot anyway, which is an empty sixth of the band
     and is what showed on the deployed site (A9.2, #175).

     Not `repeat(var(--n), 1fr)`: the narrow breakpoints below want min(n, 3),
     and repeat() does not take a math function as its count. Not `auto-fit`,
     which would change the layout at every width instead of only the one that
     is wrong. With all six present this is identical to what it replaced. */
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: 2rem 1.5rem;
  padding-bottom: 2.5rem;
  margin-bottom: 2.5rem;
  border-bottom: 1px solid var(--line-on-dark);
}

/* ---------- Popular destinations ---------- */

.footer__destinations {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: .75rem 1.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* A row is two facts, not one long name: "Montreal" answers where and "Canada"
   answers where that is. Run together on one line they read as a single place
   name nobody calls it. */
.footer__destination {
  display: flex;
  flex-direction: column;
  gap: .125rem;
  padding: .75rem 1rem;
  background-color: rgba(255, 255, 255, .04);
  border-radius: .5rem;
  text-decoration: none;
  transition: background-color var(--dur-fast) ease;
}

.footer__destination-city {
  color: var(--ink-on-dark-lead);
  font-size: .9375rem;
  font-weight: 600;
}

.footer__destination-country {
  color: var(--ink-on-dark);
  font-size: .8125rem;
}

@media (hover: hover) and (pointer: fine) {
  .footer__destination:hover { background-color: rgba(255, 255, 255, .08); }
}

/* ---------- Brand, socials, and the two short columns ---------- */

.footer__bottom {
  display: grid;
  /* Brand, two short columns, and the card. The card is given a fixed share
     rather than a fraction so the field inside it does not shrink to a stub on
     a middling window while the brand block, which is only text, keeps growing.

     24rem, because the card's own padding and the Subscribe button take 149px
     of it before the field gets any: at 20rem that left 171px, which is shorter
     than an ordinary work address and made everyone type into a keyhole. */
  grid-template-columns: minmax(0, 1.2fr) minmax(0, .8fr) minmax(0, .8fr) 24rem;
  gap: 2rem 1.5rem;
  padding-bottom: 2rem;
  align-items: start;
}

/* The mark beside both lines. `center` against .footer__brand-lines and not
   against the name, which is the whole difference: the two lines are one block
   here, so the tile is measured against their combined height and sits level
   with the pair. */
.footer__brand-head {
  display: flex;
  align-items: center;
  gap: .875rem;
  /* Clear of the socials below. 20px read as the next line of the same block --
     the same distance a column leaves between its heading and its first link --
     where these are two separate things that happen to share a corner. */
  margin-bottom: 2.25rem;
}

/* A filled tile rather than a bare glyph, which is what makes the logo read as
   a logo beside two lines of text instead of a bullet in front of one. Square,
   and sized to the pair: 2.75rem is about what the name and the years measure
   together. */
.footer__brand-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 2.75rem;
  height: 2.75rem;
  color: var(--on-brand);
  font-size: 1.25rem;
  background-color: var(--brand-accent-fill);
  border-radius: .875rem;
}

.footer__brand-lines {
  display: grid;
  gap: .125rem;
  min-width: 0;
}

.footer__brand-name {
  color: var(--ink-on-dark-lead);
  font-size: 1.125rem;
  font-weight: 700;
  text-decoration: none;
}

.footer__copyright {
  margin: 0;
  color: var(--ink-on-dark);
  font-size: .875rem;
}

.footer__socials {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Square targets rather than icon-sized ones: 2.5rem clears the 24px minimum a
   pointer needs, and the tile is what makes a bare glyph look deliberate. */
.footer__social {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  color: var(--ink-on-dark);
  background-color: rgba(255, 255, 255, .06);
  border-radius: .625rem;
  transition: color var(--dur-fast) ease, background-color var(--dur-fast) ease;
}

@media (hover: hover) and (pointer: fine) {
  .footer__social:hover {
    color: var(--ink-on-dark-lead);
    background-color: rgba(255, 255, 255, .12);
  }
}

/* ---------- The subscribe form ---------- */

/* Every property here is restated rather than inherited. The reset at the top
   of this file strips borders and margins from inputs and buttons and sets a
   global 18px on fields, and none of Bootstrap's .form-control is in play --
   so anything not written here is not what it looks like it is. */
.footer__subscribe {
  padding: 1.25rem;
  /* A fill and not the text token it used to name. `--brand-ink-700` is the
     emphasis *ink* now, which the dark palette lifts to a pale blue -- so this
     panel came out pale blue with pale text on it. It wants the band one step
     up from the footer, which is what `--brand-ink-fill-700` is. */
  background-color: var(--brand-ink-fill-700);
  border-radius: .75rem;
}

.footer__subscribe-text {
  margin: 0 0 .875rem;
  color: var(--ink-on-dark);
  font-size: .875rem;
}

.footer__subscribe-row {
  display: flex;
  gap: .5rem;
}

.footer__subscribe-input {
  flex: 1 1 auto;
  min-width: 0;
  height: 2.5rem;
  padding: 0 .75rem;
  color: var(--brand-ink);
  font-size: .875rem;
  background-color: var(--surface-raised);
  border: 1px solid transparent;
  border-radius: .5rem;
}

.footer__subscribe-input::placeholder { color: var(--ink-faint); }

/* Keyboard focus, for the whole band at once.

   This rule used to name only .footer__subscribe-input, so one of the footer's
   73 tab stops had a ring and 72 did not -- the densest keyboard surface on the
   site, and the only one nobody had styled. Everything else in the app has one:
   the stylesheet designs :focus-visible for ten other components.

   The colour is the one that rule already chose, and it is the right one for
   the reason it was chosen: --ink-on-dark-lead is 11.98 against the navy, where
   the --brand-accent used on white pages is a dark teal on a dark blue.

   Written against the elements rather than the classes so the legal band's six
   unclassed links are covered too. */
.footer a:focus-visible,
.footer button:focus-visible,
.footer input:focus-visible {
  outline: 2px solid var(--ink-on-dark-lead);
  outline-offset: 2px;
}

.footer__subscribe-submit {
  flex: 0 0 auto;
  height: 2.5rem;
  padding: 0 1rem;
  color: var(--on-brand);
  font-size: .875rem;
  font-weight: 600;
  background-color: var(--brand-accent-fill);
  border-radius: .5rem;
  transition: background-color var(--dur-fast) ease, transform var(--dur-fast) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .footer__subscribe-submit:hover { background-color: var(--brand-accent-fill-700); }
}

.footer__subscribe-submit:active { transform: scale(.97); }

/* Disabled while the request is out. Without this the button still looks
   pressable and a second click sends a second address. */
.footer__subscribe-submit:disabled {
  opacity: .6;
  cursor: default;
  transform: none;
}

/* The answer. Empty until there is one, so it takes no room on a form nobody
   has used yet -- but it is in the flow, not absolute, so the card grows to
   hold it rather than the text landing on top of the consent line. */
.footer__subscribe-note {
  margin: .625rem 0 0;
  font-size: .8125rem;
}

.footer__subscribe-note:empty { display: none; }

.footer__subscribe-note[data-tone="good"]  { color: var(--good-on-dark); }
.footer__subscribe-note[data-tone="bad"]   { color: var(--bad-on-dark); }
.footer__subscribe-note[data-tone="quiet"] { color: var(--ink-on-dark); }

.footer__subscribe-consent {
  margin: .625rem 0 0;
  color: var(--ink-on-dark);
  font-size: .75rem;
  opacity: .8;
}

/* ---------- The currency switcher ---------- */

.currency { position: relative; }

/* Sized and coloured like the nav links beside it, because it sits in their
   row -- but a button, because it opens something rather than going somewhere. */
.currency__trigger {
  display: inline-flex;
  align-items: center;
  padding: .5rem .75rem;
  border: 0;
  border-radius: .5rem;
  background-color: transparent;
  color: inherit;
  font-size: 1rem;
  font-weight: 500;
  cursor: pointer;
  transition: background-color var(--dur-fast) ease;
}

@media (hover: hover) and (pointer: fine) {
  .currency__trigger:hover { background-color: rgba(255, 255, 255, .12); }
}

.currency__trigger[aria-expanded="true"] { background-color: rgba(255, 255, 255, .16); }

/* The theme toggle: a track with the sun or the moon riding on it.

   None of the colours here is a palette token, and that is the point. This
   draws the two themes rather than belonging to one, so the night stays navy on
   a light page and the day stays blue on a dark one. A control that restyled
   itself with the thing it controls would stop being a picture of the choice.

   It grows to the left because it is pinned to the right edge -- the only
   direction that leaves it under the pointer that reached for it. Width is what
   moves, and on a fixed element with nothing beside it that is a repaint of one
   box, not a reflow. */
:root {
  --toggle-size: 2.75rem;
  --toggle-open: 7.25rem;   /* room for a word beside the disc */
  --toggle-inset: .25rem;
  --toggle-disc: calc(var(--toggle-size) - var(--toggle-inset) * 2);

  /* The two tracks, and neither is a sky any more. The night one was already
     close to the brand navy and read as native; the day one was a light blue
     that appears nowhere else in this palette, so collapsed it was a foreign
     blue dot in the corner. Pale and dark say "light" and "dark" without
     needing a metaphor to carry it. */
  --track-light: linear-gradient(160deg, #EFF3F8 0%, #D9E1EC 100%);
  --track-dark: linear-gradient(160deg, #16233C 0%, #0B1426 100%);
  --disc-face: radial-gradient(circle at 35% 30%, #FFFFFF 0%, #F4F6FA 55%, #DDE3EC 100%);
}

.theme-toggle {
  position: fixed;
  right: 1.25rem;
  bottom: 1.25rem;
  z-index: 1030;
  width: var(--toggle-size);
  height: var(--toggle-size);
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: none;
  box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .28);
  cursor: pointer;
  transition: width var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out),
              box-shadow var(--dur-fast) ease;
}

/* Focus opens it as well as hover. A keyboard never hovers, and half a control
   that only exists under a pointer is half a control. */
.theme-toggle:hover,
.theme-toggle:focus-visible {
  width: var(--toggle-open);
  box-shadow: 0 .5rem 1.25rem rgba(0, 0, 0, .34);
}

.theme-toggle:focus-visible {
  outline: 2px solid var(--brand-accent);
  outline-offset: 3px;
}

/* Out of the way of "back to top". A transform, so the lift composites and does
   not fight the width beside it. */
.theme-toggle.is-raised {
  transform: translateY(calc(-1 * var(--toggle-size) - .625rem));
}

/* The sky. Both gradients are painted, and the one that is not wanted is faded
   out -- crossfading two layers rather than animating a gradient, which is not
   interpolable. */
.theme-toggle__track,
.theme-toggle__track::after {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background-image: var(--track-light);
}

.theme-toggle__track::after {
  content: "";
  background-image: var(--track-dark);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}

.theme-toggle[aria-pressed="true"] .theme-toggle__track::after { opacity: 1; }

/* The word for the state you are in, in the half the disc is not covering.

   This replaced a set of stars and a small cloud. Both were nicer to look at
   and neither said which way the switch was thrown, which is the one thing a
   switch has to say. */
.theme-toggle__word {
  position: absolute;
  top: 50%;
  width: calc(var(--toggle-open) - var(--toggle-disc) - var(--toggle-inset) * 2);
  opacity: 0;
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  transform: translateY(-50%);
  transition: opacity var(--dur-fast) var(--ease-out);
}

/* Opposite the disc: by day it sits left, so the word goes right. */
.theme-toggle__word--light {
  right: var(--toggle-inset);
  color: #0E2136;
}

.theme-toggle__word--dark {
  left: var(--toggle-inset);
  color: #E7ECF5;
}

/* Only the one that matches, and only once there is a half to put it in. */
.theme-toggle[aria-pressed="true"] .theme-toggle__word--light,
.theme-toggle[aria-pressed="false"] .theme-toggle__word--dark { opacity: 0 !important; }

.theme-toggle[aria-pressed="false"]:hover .theme-toggle__word--light,
.theme-toggle[aria-pressed="false"]:focus-visible .theme-toggle__word--light,
.theme-toggle[aria-pressed="true"]:hover .theme-toggle__word--dark,
.theme-toggle[aria-pressed="true"]:focus-visible .theme-toggle__word--dark { opacity: 1; }

/* The disc is the sun or the moon, and where it sits is the state: left is day,
   right is night. Closed, the two coincide -- it fills the circle and has
   nowhere to be, which is why `left` is the only thing that travels. */
.theme-toggle__disc {
  position: absolute;
  top: var(--toggle-inset);
  left: var(--toggle-inset);
  width: var(--toggle-disc);
  height: var(--toggle-disc);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background-image: var(--disc-face);
  box-shadow: 0 .0625rem .25rem rgba(0, 0, 0, .35);
  transition: left var(--dur-base) var(--ease-out), transform var(--dur-fast) var(--ease-out);
}

/* The sun and the moon themselves, and the reason there is a disc to put them
   on rather than a bare icon: a glyph alone on the sky had nothing to separate
   it from the sky, and a graded circle alone is a circle. The disc gives the
   shape, the glyph gives the meaning. */
.theme-toggle__glyph {
  position: absolute;
  font-size: .9375rem;
  line-height: 1;
  transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}

/* #E07C00 was the first pick and measured 2.75 on the disc -- under the 3:1 a
   graphic needs, which for the one glyph carrying the meaning is not a detail.
   This is 3.77 and still reads as sunlight rather than as rust. */
.theme-toggle__glyph--sun { color: #C06600; }
.theme-toggle__glyph--moon { color: #1B2A4A; }

/* Day: the sun is up. The one leaving turns as it goes, so the swap reads as
   one thing becoming another rather than two things trading places. */
.theme-toggle__glyph--moon { opacity: 0; transform: rotate(70deg) scale(.55); }
.theme-toggle__glyph--sun { opacity: 1; transform: none; }

.theme-toggle[aria-pressed="true"] .theme-toggle__glyph--sun {
  opacity: 0;
  transform: rotate(-70deg) scale(.55);
}

.theme-toggle[aria-pressed="true"] .theme-toggle__glyph--moon {
  opacity: 1;
  transform: none;
}

.theme-toggle[aria-pressed="true"]:hover .theme-toggle__disc,
.theme-toggle[aria-pressed="true"]:focus-visible .theme-toggle__disc {
  left: calc(var(--toggle-open) - var(--toggle-disc) - var(--toggle-inset));
}

.theme-toggle:active .theme-toggle__disc { transform: scale(.92); }

@media (max-width: 575.98px) {
  .theme-toggle {
    right: .75rem;
    bottom: .75rem;
  }
}




/* Right-aligned to the trigger, because the trigger is at the right end of the
   header and a panel hanging off its left edge would leave the screen. */
.currency__panel {
  position: absolute;
  top: calc(100% + .5rem);
  right: 0;
  z-index: 1050;
  width: 20rem;
  padding: .75rem;
  border: 1px solid var(--line);
  border-radius: .75rem;
  background-color: var(--surface-raised);
  box-shadow: 0 12px 32px rgba(16, 36, 63, .18);
  color: var(--ink-body);
}

.currency__filter {
  width: 100%;
  margin-bottom: .5rem;
  padding: .5rem .75rem;
  border: 1px solid var(--line-strong);
  border-radius: .5rem;
  font-size: .9375rem;
}

.currency__filter:focus-visible {
  outline: 2px solid var(--brand-accent);
  outline-offset: 1px;
}

/* Capped and scrolled: thirty currencies is taller than most screens, and a
   panel that runs off the bottom cannot reach its own source line. */
.currency__list {
  max-height: 17rem;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  list-style: none;
}

.currency__option {
  display: flex;
  gap: .5rem;
  align-items: baseline;
  width: 100%;
  padding: .4375rem .5rem;
  border: 0;
  border-radius: .375rem;
  background-color: transparent;
  color: inherit;
  font-size: .9375rem;
  text-align: left;
  cursor: pointer;
}

@media (hover: hover) and (pointer: fine) {
  .currency__option:hover { background-color: var(--surface); }
}

.currency__option:focus-visible {
  outline: 2px solid var(--brand-accent);
  outline-offset: -2px;
}

/* The code is what somebody scans for, so it is the fixed column. */
.currency__code {
  flex: 0 0 2.75rem;
  font-weight: 700;
}

.currency__name { flex: 1 1 auto; color: var(--ink-secondary); }
.currency__symbol { flex: 0 0 auto; color: var(--ink-muted); }
.currency__option.is-active .currency__name { color: var(--ink-body); }
.currency__tick { flex: 0 0 auto; color: var(--brand-accent); }

/* Where the numbers come from and how old they are. Quiet, because it is a
   provenance note rather than something the page is trying to say -- but
   present, because a converted price is an estimate at somebody else's rate. */
.currency__source {
  margin: .5rem 0 0;
  padding-top: .5rem;
  border-top: 1px solid var(--line);
  color: var(--ink-muted);
  font-size: .75rem;
}

/* The rate line, under the sentence that says whose rates these are. */
.currency__history { margin: .4rem 0 0; }

/* Stretched to the panel's width rather than drawn at its own: the shape is
   the point and 220 units of viewBox are not 220 pixels of anybody's panel. */
.currency__spark {
  display: block;
  width: 100%;
  height: 36px;
}

.currency__spark polyline {
  fill: none;
  stroke: var(--ink-muted);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  /* The stroke is scaled with the box by preserveAspectRatio="none", which
     would otherwise make a wide panel draw a hairline. */
  vector-effect: non-scaling-stroke;
}

.currency__history-note {
  color: var(--ink-muted);
  font-size: .6875rem;
  line-height: 1.4;
}

/* Up is the chosen currency buying more per dollar, which is good news for a
   Canadian traveller -- so green is the honest colour for it here. The word
   and the sign say it too, because a percentage read out on its own is no
   worse for having no colour. */
.currency__change--up { color: var(--brand-success); }
.currency__change--down { color: var(--brand-danger); }
.currency__change--flat { color: var(--ink-muted); }

/* Hidden while the panel is shut, so the filter and thirty buttons are not in
   the tab order of every page on the site. */
.currency__option[hidden] { display: none; }

/* ---------- The cookie notice ---------- */

/* A light strip rather than the dark slab this started as. The dark band read
   as a piece of the footer that had floated up the page, and it took 96px at
   desktop and 210px on a phone to ask a one-line question. White, one line, one
   row of buttons: the bar should be the least interesting thing on screen,
   because the page behind it is what somebody came for. */
.cookie-notice {
  /* The pill height, and the icon is squared off it. Declared once because
     "the same size as the buttons" is a relationship, and two hardcoded
     numbers agreeing today is not one. */
  --notice-control: 2.5625rem;
  position: fixed;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 1040;
  padding: .8125rem 0;
  border-top: 1px solid var(--line);
  background-color: var(--surface-raised);
  /* Upward and barely there. The border does the separating; this only stops a
     white bar dissolving into a white page. */
  box-shadow: 0 -2px 12px rgba(16, 36, 63, .06);
}

/* The row itself, on the site's grid. The band above is full-bleed so the
   background reaches both edges; this keeps the writing where the writing goes
   everywhere else. */
.cookie-notice__inner {
  display: flex;
  flex-wrap: wrap;
  gap: .625rem 2rem;
  align-items: center;
  justify-content: space-between;
}

/* Icon and words travel together, so the pair wraps above the buttons as one
   thing. The basis lives here now rather than on the paragraph. */
.cookie-notice__message {
  display: flex;
  flex: 1 1 20rem;
  gap: .75rem;
  /* Centred now the icon is pill-sized. Top-aligned made sense at 22px, when
     the icon was shorter than a line of text and could sit on the first one;
     at 41px it is taller than any single line, so aligning to the top left the
     text hanging off its shoulder. */
  align-items: center;
}

.cookie-notice__text {
  /* No reading-measure cap: this is one line of interface text and the row
     already bounds it, where a cap only forced a wrap the bar did not need. */
  flex: 1 1 auto;
  margin: 0;
  color: var(--ink-secondary);
  font-size: .9375rem;
  line-height: 1.45;
}

.cookie-notice__text a { color: var(--brand-accent); }

/* Fixed size and never shrinking: a flex item with an intrinsic aspect ratio
   will otherwise squash before the text does. The nudge sets it on the first
   line's cap height rather than the top of its line box. */
.cookie-notice__icon {
  flex: 0 0 auto;
  width: var(--notice-control);
  height: var(--notice-control);
}

.cookie-notice__actions {
  display: flex;
  flex: 0 0 auto;
  flex-wrap: wrap;
  gap: .5rem;
}

/* Pills, like every other action on this site. Both are the same size, the same
   shape and in the same row; the only difference is the fill. A notice where
   "no" is small print, or a link, or a shade of grey away from the background,
   is a notice designed to be misread -- and this one is a real gate, so the
   answer has to be a real choice. */
.cookie-notice__button {
  /* Its padding already produced this height; stating it is what stops a font
     or padding change moving the buttons without moving the icon. */
  min-height: var(--notice-control);
  padding: .5rem 1.125rem;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background-color: var(--surface-raised);
  color: var(--ink-secondary);
  font-size: .9375rem;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: border-color var(--dur-fast) ease, background-color var(--dur-fast) ease, color var(--dur-fast) ease;
}

.cookie-notice__button--yes {
  border-color: var(--brand-accent);
  background-color: var(--brand-accent-fill);
  color: var(--on-brand);
}

@media (hover: hover) and (pointer: fine) {
  .cookie-notice__button:hover { border-color: var(--brand-accent); color: var(--brand-accent); }
  .cookie-notice__button--yes:hover {
    border-color: var(--brand-accent-700);
    background-color: var(--brand-accent-fill-700);
    color: var(--on-brand);
  }
}

/* Out of the way rather than shuffled above the bar: the bar's height depends
   on how the text wraps, so any offset here would be a number that is right at
   one width and wrong at the next. The button comes back the moment the
   question is answered, which is one click away. */
/* Both of them, not just one. The notice sits across the bottom of the window
   and these two share that corner, so a toggle left behind would be the one
   thing parked on top of the bar asking to be read. */
body:has(.cookie-notice) .to-top,
body:has(.cookie-notice) .theme-toggle { display: none; }

/* The way back to the question, on the cookies page. A button and not a link,
   because it changes something rather than going somewhere. */
.cookie-reset {
  padding: .5rem 1.125rem;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background-color: var(--surface-raised);
  color: var(--ink-body);
  font-size: .9375rem;
  font-weight: 600;
  cursor: pointer;
  transition: border-color var(--dur-fast) ease, color var(--dur-fast) ease;
}

@media (hover: hover) and (pointer: fine) {
  .cookie-reset:hover { border-color: var(--brand-accent); color: var(--brand-accent); }
}

/* ---------- Legal, and what the page cost to draw ---------- */

.footer__legal {
  padding-top: 1.5rem;
  border-top: 1px solid var(--line-on-dark);
  color: var(--ink-on-dark);
  font-size: .8125rem;
}

.footer__legal p { margin: 0 0 .5rem; }
.footer__legal a { color: var(--ink-on-dark-lead); }

/* Underline alone, and gated like every other footer hover -- on a touchscreen
   :hover latches on tap. The colour lift that marks a column link has nothing
   left to do down here: these already sit at the lead colour. Covers the prose
   line too, which had no hover state at all. */
@media (hover: hover) and (pointer: fine) {
  .footer__legal a:hover { text-decoration: underline; }
}

/* The three legal pages, above the prose line rather than inside it. A row of
   its own because that is where a reader looks for them, and because a link
   buried in a sentence about who built the site does not read as one. */
.footer__legal-links {
  display: flex;
  flex-wrap: wrap;
  gap: .375rem 1.25rem;
  margin: 0 0 .75rem;
  padding: 0;
  list-style: none;
  font-weight: 500;
}

/* A step quieter than the line above it. This is a note to whoever is curious
   about the build, not something the page is trying to tell anyone. */
.footer__stats {
  color: var(--ink-on-dark);
  font-size: .75rem;
  opacity: .75;
}

/* ---------- Narrowing ---------- */

@media (max-width: 1279.98px) {
  /* Back to rows here. A short last row reads as an ordinary wrap rather than
     as the hole a single squeezed row would leave. */
  .footer__columns { grid-auto-flow: row; grid-template-columns: repeat(3, 1fr); }
}

@media (max-width: 1279.98px) {
  /* The card drops to a row of its own before the columns do. It is the widest
     thing in the band and the first to make the other three too narrow to read. */
  .footer__bottom { grid-template-columns: minmax(0, 1.2fr) minmax(0, .8fr) minmax(0, .8fr); }
  .footer__subscribe { grid-column: 1 / -1; max-width: 28rem; }
}

@media (max-width: 991.98px) {
  .footer__bottom { grid-template-columns: 1fr 1fr; }
  /* The brand sits over both columns rather than squeezing into one, so the
     social row keeps its shape instead of wrapping to three lines. */
  .footer__brand { grid-column: 1 / -1; }
}

@media (max-width: 767.98px) {
  .footer__columns,
  .footer__destinations { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 575.98px) {
  .footer__inner { padding-top: 2rem; }
  .footer__columns,
  .footer__destinations,
  .footer__bottom { grid-template-columns: 1fr; }
}

/* ==========================================================================
   #PLACE PAGE
   ==========================================================================
   A rail and a stack of blocks. The rail is a table of contents for a page
   that is long by design -- somebody who came for the airports should not have
   to scroll past the fares to find them.

   Not city-specific, which is why it is not called that. A country is read the
   same way and so, next, are an airline and an airport: some facts, some
   fares, some links, and a rail to reach them by. */

.place {
  padding: 2rem 0 4rem;
}

.place__layout {
  display: grid;
  grid-template-columns: 17rem minmax(0, 1fr);
  gap: 1.5rem;
  align-items: start;
}

/* Sticky, and bounded by .place__layout rather than by the page: it travels
   down beside the blocks and stops where they stop, instead of riding on over
   the footer. Same reason the search bar is bounded by .page. */
.place__rail {
  position: sticky;
  top: 1.5rem;
  padding: 1.25rem;
  background-color: var(--surface-raised);
  border-radius: 1rem;
}

.place__rail-title {
  margin: 0 0 1rem;
  font-size: 1.125rem;
  font-weight: 700;
}

.place__rail-list {
  display: grid;
  gap: .25rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.place__rail-link {
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: .5rem;
  color: var(--ink-body);
  font-size: .9375rem;
  text-decoration: none;
  border-radius: .625rem;
  transition: background-color var(--dur-fast) ease;
}

@media (hover: hover) and (pointer: fine) {
  .place__rail-link:hover { background-color: var(--surface-sunken); }
}

/* The icon tile is what makes a list of eight grey rows scannable: the colour
   is the thing the eye comes back to, not the word. */
.place__rail-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 2rem;
  height: 2rem;
  color: var(--on-brand);
  font-size: .8125rem;
  border-radius: 50%;
}

/* A colour per block, from the categorical set. These were four hues invented
   on the spot -- a #2F9E44, an #E8590C, a #C2255C and a #1971C2, none of them
   from this palette. Flattening them was the wrong answer -- a rail you can
   find by colour is easier to come back to than one you have to read -- and so
   was muting them. They are those four colours again, named this time, and
   measured.

   Five now that the city and airport pages have a routes block, and the fifth
   is the crimson those four started out with -- the one violet replaced. Same
   ramp as the rest of the set, at the same step. */
.place__rail-icon--green  { background-color: var(--cat-green); }
.place__rail-icon--blue   { background-color: var(--cat-blue); }
.place__rail-icon--orange { background-color: var(--cat-orange); }
.place__rail-icon--violet { background-color: var(--cat-violet); }
.place__rail-icon--pink   { background-color: var(--cat-pink); }

.place__blocks {
  display: grid;
  gap: 1.5rem;
  min-width: 0;
}

.place__block {
  padding: 1.75rem;
  background-color: var(--surface-raised);
  border-radius: 1rem;
  /* A grid item is min-width:auto, which means "at least as wide as what is
     inside me" -- and inside one of these is a strip of eight fare cards that
     would rather be 1,520px. Without this the widest block sets the width of
     the column and every other block is dragged out with it, off the side of
     the page. The strip's own overflow-x can only work once this lets the
     block be narrower than its contents. */
  min-width: 0;
  /* Clear of the sticky rail's top edge, so an anchored jump does not land
     with the heading tucked under it. */
  scroll-margin-top: 1.5rem;
}

.place__block-title {
  margin: 0 0 1.25rem;
  font-size: 1.5rem;
  font-weight: 700;
}

/* The way home, under the route's figures. Quiet: it is an offer rather than
   the answer this page came to give, and the price is what makes it worth
   reading at all. */
.route-back {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .375rem;
  margin: 1.25rem 0 0;
  color: var(--ink-secondary);
  font-size: .9375rem;
}

.route-back__icon { color: var(--ink-muted); }

.route-back__link:any-link {
  color: var(--brand-accent-700);
  font-weight: 600;
  text-decoration: underline;
  text-decoration-color: var(--line-strong);
  text-underline-offset: .15em;
}

.route-back__link:hover { text-decoration-color: currentcolor; }

/* The map, whether it is a live one or the picture inside <noscript>.

   `aspect-ratio` rather than a height, and the same 650:300 the picture has:
   the box then has its final shape before Mapbox GL loads, so the page does not
   jump when half a megabyte of JavaScript arrives and fills it. It is also what
   gives the map a size to initialise into -- GL JS measures its container, and
   a container with no height renders nothing at all.

   `overflow: hidden` because the live map draws its own canvas and controls,
   which would otherwise sit square over the rounded corners. */
.place__map {
  display: block;
  width: 100%;
  aspect-ratio: 650 / 300;
  height: auto;
  margin-top: 1.25rem;
  border-radius: .75rem;
  overflow: hidden;
  /* Something to look at while the tiles arrive, in place of a white hole. */
  background-color: var(--surface-sunken);
}

/* An airline's mark. The one place page with no map of its own to lead with,
   and the one whose subject has a face people recognise faster than its name. */
/* The airline masthead: the mark and the two things that identify the carrier,
   on one line. Both used to be tiles in the grid below -- a code and a country
   sitting among counted figures, where they read as statistics and are not.
   The mark was on a line of its own above them, belonging to nothing. */
.masthead {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  margin-bottom: 1.5rem;
}

/* As tall as the three rows beside it, which measure 81px to its 80. It was
   3.5rem while the identity was two lines; the contacts moved up from below the
   tiles, where they were a row belonging to nothing, and gave the mark a
   third. */
.masthead__mark {
  flex: 0 0 auto;
  width: 5rem;
  height: 5rem;
  border-radius: 1rem;
}

.masthead__identity { min-width: 0; }

.masthead__name {
  margin: 0;
  color: var(--brand-ink);
  font-size: 1.25rem;
  font-weight: 700;
  line-height: 1.2;
}

.masthead__meta {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin: .25rem 0 0;
  color: var(--ink-muted);
  font-size: .9375rem;
}

/* The code set apart, because it is an identifier rather than a word: two
   capitals in running text read as an abbreviation of the name beside them. */
.masthead__code {
  padding: .0625rem .375rem;
  color: var(--ink-secondary);
  font-weight: 700;
  letter-spacing: .04em;
  background-color: var(--surface-sunken);
  border-radius: .25rem;
}

.masthead__sep { color: var(--line-strongest); }

/* Every link in the masthead, which is the country and the website. Plain
   underlined text rather than the chips these used to be: a chip is a thing to
   pick from a set, and there is no set here -- one country, one website, and a
   phone number that is not a link at all.

   `:any-link` and not the bare class, because the reset's `a:link` takes the
   underline off and is a class plus an element to this rule's one class. The
   pseudo-class buys the point that wins it. */
.masthead__link:any-link {
  color: var(--ink-muted);
  text-decoration: underline;
  text-decoration-color: var(--line-strong);
  text-underline-offset: .15em;
}

.masthead__link:hover { color: var(--brand-accent-700); }

/* An address has no spaces to break at, and the longest here is 57 characters
   against a text column that is 218px wide on a phone. Left alone it would
   push the mark's row wider than the card. */
.masthead__link {
  min-width: 0;
  overflow-wrap: anywhere;
}

.masthead__contact {
  display: flex;
  align-items: center;
  gap: .5rem;
  /* Wraps rather than pushing the mark's row wide: a name and a number are two
     unrelated things and read fine on two lines. */
  flex-wrap: wrap;
  margin: .375rem 0 0;
  color: var(--ink-muted);
  font-size: .9375rem;
}

/* Small, and after the words rather than beside them: it marks where the link
   goes, it is not part of the label. */
.masthead__out {
  margin-left: .375rem;
  font-size: .75em;
}

/* A pin's name, sitting above it on the map.

   White ground rather than a text halo, because a label here crosses coastline,
   motorway and parkland within its own width and no outline reads on all
   three. The map is what it sits on, so it takes the map's job of being legible
   seriously: solid ground, ink text, and a shadow to lift it off the tiles.

   `pointer-events: none` so the label never eats a drag: a map you cannot pan
   because you grabbed a word is worse than an unlabelled one. `!important`
   because Mapbox writes `pointer-events: auto` as an inline style onto the
   element it is handed -- which is this one -- and an inline style beats any
   rule here however specific. Measured: without it the computed value is
   `auto`, whatever this file says. */
.map-label {
  display: block;
  max-width: 12rem;
  padding: .1875rem .4375rem;
  overflow: hidden;
  color: var(--brand-ink);
  font-size: .8125rem;
  font-weight: 700;
  line-height: 1.25;
  white-space: nowrap;
  text-overflow: ellipsis;
  background-color: var(--surface-raised);
  border-radius: .3125rem;
  box-shadow: 0 1px 3px rgb(16 36 63 / 28%);
  pointer-events: none !important;
}

/* ---------- Which pin is which ---------- */

.place__legend {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem 1.5rem;
  margin: .75rem 0 0;
  padding: 0;
  list-style: none;
  font-size: .9375rem;
}

.place__legend-item {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
}

.place__legend-item::before {
  content: "";
  width: .625rem;
  height: .625rem;
  flex-shrink: 0;
  border-radius: 50%;
  background-color: var(--legend-pin);
}

/* Sampled from the rendered pins rather than picked to taste, and so
   deliberately outside this palette: a dot that only nearly matches the pin it
   stands for is worse than no dot, because the reader has to decide whether
   they are the same thing. */
.place__legend-item--from { --legend-pin: #0EB600; }
.place__legend-item--to { --legend-pin: #EC3735; }

.place__legend-role { color: var(--ink-muted); }

/* ---------- The four figures ---------- */

.facts-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.facts-grid__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .25rem;
  padding: 1.25rem 1rem;
  text-align: center;
  background-color: var(--surface-sunken);
  border-radius: .75rem;
}

.facts-grid__icon { color: var(--brand-accent); font-size: 1.25rem; }
.facts-grid__value { font-size: 1.0625rem; font-weight: 700; }
.facts-grid__label { color: var(--ink-muted); font-size: .875rem; }

/* ---------- Chips ---------- */

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: .625rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .625rem 1rem;
  color: var(--ink-body);
  font-size: .9375rem;
  font-weight: 600;
  background-color: var(--surface-sunken);
  border-radius: 2rem;
  text-decoration: none;
  transition: background-color var(--dur-fast) ease;
}

.chip__icon { color: var(--ink-secondary); font-size: .875rem; }

/* A widebody, on the aircraft list. The one attribute of an aircraft a
   passenger feels, and the thing that makes two airlines' lists look different
   at a glance rather than only to somebody who knows what a Dash 8 is.

   Below .chip and not above it: both are one class, so the background it sets
   wins on source order alone. */
.chip--wide {
  color: var(--brand-ink);
  background-color: var(--brand-tint);
  box-shadow: inset 0 0 0 1px var(--brand-tint-edge);
}

.chip--wide .chip__icon { color: var(--brand-accent); }

/* What a count of flights is a count of. Two blocks on the airline page look a
   fortnight ahead rather than over the whole schedule, and a number with no
   period behind it is not a number. */
.chips__note {
  margin: .875rem 0 0;
  color: var(--ink-muted);
  font-size: .8125rem;
}


/* An airline's mark, where a chip names an airline rather than a place. Square
   and small: it stands in for the icon, so it takes the icon's room. */
.chip__logo {
  width: 1.25rem;
  height: 1.25rem;
  border-radius: .25rem;
}

/* A row of chips inside a tabset. The shown panel is given whatever display
   its container names, and .chips is a flex row -- without this it would be
   handed the `block` default and every chip would take its own line. */
.chips.tabset__panel { --tabset-panel: flex; }

/* Quieter than the name it follows: the distance qualifies the city, it is not
   a second thing to read. */
.chip__note { color: var(--ink-muted); font-weight: 400; }

@media (hover: hover) and (pointer: fine) {
  .chip--link:hover { background-color: var(--line); }
}

/* ---------- Narrowing ---------- */

@media (max-width: 991.98px) {
  /* The rail stops leading and becomes a strip across the top: a sticky column
     is worth a screen's width only while there is a screen's width to spare. */
  .place__layout { grid-template-columns: minmax(0, 1fr); }
  .place__rail { position: static; }
  .place__rail-list { grid-auto-flow: column; grid-auto-columns: max-content; overflow-x: auto; }
}

@media (max-width: 767.98px) {
  .facts-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .place__block { padding: 1.25rem; }
}

/* ---------- Tabs, without JavaScript ---------- */

/* A radio group and sibling selectors do the whole job. It is keyboard-operable
   because radios are, it is announced because radios are, and it works with
   scripting off -- which matters on pages whose entire purpose is to be arrived
   at cold from a search engine.

   Named for the mechanism rather than the fares strip it was written for,
   because a schedule board wants exactly this and copying forty lines of it
   would be the wrong way to get one. */

/* The radios are the control -- kept off screen rather than display:none, which
   would take them out of the tab order and leave the labels operable by mouse
   only. */
.tabset__radio {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

.tabset__tabs {
  display: flex;
  gap: .25rem;
  margin-bottom: 1.25rem;
}

.tabset__tab {
  padding: .5rem 1rem;
  color: var(--ink-secondary);
  font-size: .9375rem;
  font-weight: 600;
  border-radius: 2rem;
  cursor: pointer;
  transition: background-color var(--dur-fast) ease, color var(--dur-fast) ease;
}

.tabset__radio:checked + .tabset__tab {
  color: var(--ink-body);
  background-color: var(--surface-sunken);
}

/* Focus lands on the radio, which cannot be seen, so the label wears the ring
   on its behalf -- otherwise tabbing to this control shows nothing at all. */
.tabset__radio:focus-visible + .tabset__tab {
  outline: 2px solid var(--brand-accent);
  outline-offset: 2px;
}

@media (hover: hover) and (pointer: fine) {
  .tabset__tab:hover { background-color: var(--surface-sunken); }
}

/* Every panel is hidden, then the one whose radio is checked is shown.

   By position, not by id. This was two rules naming #fares-home and
   #fares-away, which meant a second tabbed block anywhere on the site needed
   its own pair -- the ids of one page's tabs written into the stylesheet. The
   radios are the only inputs inside .tabset__tabs and the panels the only lists
   beside it, so :nth-of-type counts them both without knowing what either is
   called. A third tab is one more line.

   What a shown panel becomes is the caller's: a fares strip is a grid and a
   board is a plain block, so the value comes through a custom property instead
   of being decided here. */
.tabset__panel { display: none; }

.tabset:has(.tabset__radio:nth-of-type(1):checked) .tabset__panel:nth-of-type(1),
.tabset:has(.tabset__radio:nth-of-type(2):checked) .tabset__panel:nth-of-type(2) {
  display: var(--tabset-panel, block);
}

/* One tab and no radios to check: show it. */
.tabset:not(:has(.tabset__radio)) .tabset__panel { display: var(--tabset-panel, block); }

/* ---------- Schedule board ---------- */

/* A row per flight, and the columns hold their width down the whole list --
   that alignment is what makes a board scannable, and a flex row per item would
   let every row size itself and none of them line up. `auto` on the place
   column is what absorbs the difference between "Ottawa" and "Ho Chi Minh
   City". */
.board__list {
  margin: 0;
  padding: 0;
  list-style: none;
  /* Up to 114 flights in a day at Heathrow. Left to grow, the block would be
     four screens tall and the two below it unreachable without scrolling past
     all of it, so the board scrolls inside its own box instead. */
  max-height: 26rem;
  overflow-y: auto;
}

.board__row {
  display: grid;
  grid-template-columns: 3.25rem 1.5rem 4rem auto max-content;
  align-items: center;
  gap: .625rem;
  padding: .5rem .25rem;
  font-size: .9375rem;
}

.board__row + .board__row { border-top: 1px solid var(--line); }

/* Already gone, and still worth showing -- a board that shortened itself
   through the day would leave somebody wondering whether the 06:15 exists. */
.board__row--gone { color: var(--ink-muted); }
.board__row--gone .board__time { text-decoration: line-through; }

.board__time {
  color: var(--ink-body);
  font-weight: 700;
  /* Tabular, so the digits sit in a column instead of drifting by a pixel a
     row -- the one place on the site where a hundred numbers are stacked. */
  font-variant-numeric: tabular-nums;
}

.board__row--gone .board__time { color: inherit; }

.board__airline { border-radius: .25rem; }

.board__flight {
  color: var(--ink-secondary);
  font-size: .8125rem;
  font-weight: 600;
}

.board__place {
  color: var(--ink-body);
  text-decoration: none;
  /* One line, clipped: a long city name must not be allowed to make its row
     taller than every other row. */
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.board__row--gone .board__place { color: inherit; }

@media (hover: hover) and (pointer: fine) {
  .board__place:hover { text-decoration: underline; }
}

/* "to" and "from", which is what tells the two panels apart once the tab is out
   of view at the bottom of a long list. */
.board__label {
  color: var(--ink-muted);
  font-size: .8125rem;
}

.board__code {
  color: var(--ink-muted);
  font-size: .8125rem;
  font-variant-numeric: tabular-nums;
}

.board__span {
  color: var(--ink-secondary);
  font-size: .8125rem;
  white-space: nowrap;
}

@media (max-width: 575.98px) {
  /* The flight number and the duration are the two a phone can do without:
     the time, the airline mark and where it goes are the board. */
  .board__row { grid-template-columns: 3.25rem 1.5rem auto; }
  .board__flight,
  .board__span { display: none; }
}

/* ---------- Cheap fares ---------- */

.fares { --tabset-panel: grid; }

.fares__strip {
  grid-auto-flow: column;
  grid-auto-columns: minmax(16.5rem, 1fr);
  gap: 1rem;
  margin: 0;
  padding: 0 0 .5rem;
  list-style: none;
  /* A strip rather than a wrap: eight fares in a row of cards would be four
     lines of the same shape, and the horizontal run says "there are more"
     without a control that has to be built and explained. */
  overflow-x: auto;
  scroll-snap-type: x proximity;
}

.fares__item { scroll-snap-align: start; }

.fare {
  display: grid;
  gap: .375rem;
  height: 100%;
  padding: 1.25rem;
  background-color: var(--surface-sunken);
  border-radius: .75rem;
  text-decoration: none;
  transition: background-color var(--dur-fast) ease;
}

@media (hover: hover) and (pointer: fine) {
  .fare:hover { background-color: var(--line); }
}

.fare__price {
  color: var(--ink-body);
  font-size: 1.5rem;
  font-weight: 700;
  line-height: 1.1;
}

/* Beside the figure, not above it: the number is what is being compared and the
   currency is the same on every card. */
.fare__currency {
  margin-left: .25rem;
  color: var(--ink-muted);
  font-size: .8125rem;
  font-weight: 400;
}

.fare__route { color: var(--ink-body); font-size: .9375rem; font-weight: 600; }

.fare__detail {
  display: flex;
  align-items: center;
  /* Wrapping, because the row holds four things of unpredictable width -- a
     logo, a date, a duration and the word direct -- and "17 Nov," on one line
     with "Tue" under it is what a row that cannot wrap does instead. */
  flex-wrap: wrap;
  gap: .25rem .5rem;
  margin-top: .25rem;
  color: var(--ink-secondary);
  font-size: .8125rem;
}

.fare__when { white-space: nowrap; }

.fare__airline { flex: 0 0 auto; border-radius: 50%; }
.fare__legs { margin-left: auto; white-space: nowrap; }
.fare__times { color: var(--ink-muted); font-size: .8125rem; }


@media (max-width: 575.98px) {
  .fares__strip { grid-auto-columns: minmax(13rem, 1fr); }
}

/* ==========================================================================
   #ARTICLE
   ==========================================================================
   A page of prose, which is a shape this stylesheet did not have. Every other
   content family is a grid of things and takes the container; these are read a
   line at a time, so they take a measure instead.

   Shared by /about and the five /help articles -- see
   partials/article.html.twig. The README-only parts of markdown are under
   #README further up: code, tables, an image, and the heading levels the help
   articles have no use for.
   ========================================================================== */

/* Prose, not a record, so this is the one content family with no rail beside
   it. The place shell earns its rail by having four or five blocks a reader
   jumps between; an article is read top to bottom, and an anchor list over five
   headings is furniture. */
.article {
  /* The same 2rem under the trail that .place and the directory pages leave.
     Missing, the sheet's top edge sat exactly on the breadcrumb's baseline. */
  padding-top: 2rem;
  padding-bottom: 3rem;
}

.article__layout {
  display: grid;
  gap: 1.5rem;
  justify-content: center;
}

.article__sheet {
  padding: 2rem 2.25rem;
  /* The same column every other content page gives its blocks, so a help
     article and a city page hold their text in the same width. */
  max-width: var(--content-column);
  background-color: var(--surface-raised);
  border-radius: 1rem;
}

/* 2rem, the same as .directory__title and .page-head__title. It was 1.875rem
   for no reason anybody could have given -- and once every page title on the
   site came from one of these three rules, the odd 2px was the last thing
   between them and one scale. */
/* The band an article opens with, on help and legal.

   The header's own colour, not a lighter one: the two are meant to read as a
   single block that the sheet punches out of, which is how the reference this
   follows works -- its header is the same blue as its hero. A distinguishable
   band would put a seam under the header and turn one shape into two.

   Named `article__hero` and not `hero` on purpose. main.css:3543 makes the
   header sticky on any page containing `.hero`, matched on the element rather
   than on a class a page would have to carry, so taking that name would
   silently change the header's behaviour here. `:has(.hero)` is a class
   selector and not a prefix, so this does not match it -- the same reason
   `.hero-cards` does not. */
.article--hero {
  /* The band supplies the space the sheet used to leave for itself. */
  padding-top: 0;
}

.article__hero {
  /* On top of the trail's own 1.25rem, so the path clears the header rather
     than sitting under it. The band is one block with the header, which is
     the whole point of it -- and two things in one block still need space
     between them. */
  padding-top: 1.75rem;
  /* This is not the gap under the title. Two numbers decide that: the sheet
     rises by the negative margin below, so what a reader sees between the
     title and the sheet's top edge is this *less* that. Change one and the
     other has to move or the gap does. */
  padding-bottom: 12.5rem;
  background-color: var(--brand-ink-fill);
  text-align: center;
}

.article__hero .article__title {
  /* Still the sheet's flex row, with nothing beside the title to lay out --
     centred rather than stacked now that the band carries no icon. */
  justify-content: center;
  /* Wide enough for the longest title on one line -- "Changing passenger
     details", 26 characters -- and narrow enough that a longer one wraps
     instead of spanning the full container. */
  max-width: 26ch;
  margin: .5rem auto 0;
  color: var(--on-brand);
  font-size: clamp(2rem, 4vw, 2.75rem);
  line-height: 1.15;
  /* Two balanced lines rather than a long one and an orphan, where it wraps.
     Ignored by browsers that do not know it, which lose nothing. */
  text-wrap: balance;
}

.article__hero .article__title-icon {
  /* --cat-blue measures 3.11:1 on this ground, which is the floor for
     something decorative and looks it. The tint is 13.62:1 and keeps the
     accent rather than going grey. */
  color: var(--brand-tint);
  font-size: 1.75rem;
}

/* The sheet, starting inside the band and finishing outside it. Follows
   .hero-cards, the one other overlap in this file: a margin rather than a
   position, so the reading order is still band then sheet, and a z-index so
   the overlap is paint-order-independent. */
.article--hero .article__layout {
  position: relative;
  z-index: 1;
  /* Deep enough that the band runs past the lead entirely and finishes level
     with the prose below it. Shallower and the navy ends on the sheet's own
     top padding, which reads as the sheet starting below the band rather than
     inside it -- an overlap has to cross something to be one. */
  margin-top: -9rem;
}

@media (max-width: 47.9375em) {
  /* Less of both. The band is already taller once the title wraps, and a
     deep overlap on a narrow screen buries the title it sits under. */
  .article__hero { padding-top: 1rem; padding-bottom: 9.5rem; }
  .article--hero .article__layout { margin-top: -6.5rem; }
}

.article__title {
  display: flex;
  /* Baseline, not centre: "Changing passenger details" wraps to two lines below
     about 430px, and centred the icon floated between them like a bullet for
     the whole block instead of sitting with the first line. */
  align-items: baseline;
  gap: .75rem;
  margin: 0 0 .75rem;
  color: var(--brand-ink);
  font-size: 2rem;
  font-weight: 700;
  letter-spacing: -.01em;
}

.article__title-icon {
  color: var(--cat-blue);
  font-size: 1.5rem;
}

/* The summary again, as the first thing under the title -- it is also the meta
   description, so a reader arriving from a search result sees the sentence that
   brought them. Set apart from the body so it reads as the promise rather than
   as the first paragraph of the answer. */
.article__lead {
  margin: 0 0 1.75rem;
  padding-bottom: 1.25rem;
  color: var(--ink-secondary);
  font-size: 1.125rem;
  line-height: 1.6;
  border-bottom: 1px solid var(--line);
}

.article__body {
  color: var(--ink-body);
  font-size: 1rem;
  line-height: 1.7;
}

.article__body > :first-child {
  margin-top: 0;
}

/* The ladder never drops below the 1rem body text: a heading smaller than the
   paragraph it introduces reads as a caption. It is the README page's ladder,
   which was the considered one of the two -- the help pages had invented a
   flatter h2 with no rule above it, and one of them had to go. Below h3 there
   is no room left to shrink, so the deeper levels separate on weight, colour
   and letter-spacing instead of size; those live under #README, because they
   are levels only generic markdown reaches. */

/* Section headings carry the page's structure, so they take a rule above to
   separate one section from the last. */
.article__body h2 {
  margin-top: 2.5rem;
  margin-bottom: 1rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
  color: var(--brand-ink);
  font-size: 1.375rem;
  font-weight: 700;
}

/* The lead already draws a rule under itself, so a section heading that opens
   the body must not draw a second one below it. Left in, the two hairlines sat
   28px apart -- the lead's bottom margin -- and read as a mistake, which is
   what they were. */
.article__body > h2:first-child {
  padding-top: 0;
  border-top: 0;
}

.article__body h3 {
  margin-top: 1.75rem;
  margin-bottom: .35rem;
  color: var(--brand-ink);
  font-size: 1.125rem;
  font-weight: 700;
}

.article__body p,
.article__body ul,
.article__body ol {
  margin-bottom: 1rem;
}

/* Bootstrap's reboot zeroes list padding, and the markers are drawn outside the
   box, so an unpadded list hangs its numbers to the left of the text column.
   Enough room for a two-digit marker to stay inside the measure. */
.article__body ul,
.article__body ol {
  padding-left: 1.75rem;
}

.article__body li + li {
  margin-top: .35rem;
}

/* Underlined because they sit in running text, where colour alone is not
   enough to mark a link. Unclassed on purpose: a link that carries a class is a
   component -- a card on the hub, a button on the unavailable page -- and it
   says how it looks itself. Written as a plain `a` this beat `.article-card` on
   specificity and underlined all five topics on /help. */
.article__body a:not([class]) {
  color: var(--brand-accent-700);
  text-decoration: underline;
  text-underline-offset: .15em;
}

.article__body a:not([class]):hover {
  color: var(--brand-accent);
}

/* A line that belongs to the document rather than to the page: where the README
   came from, and when it last changed. */
.article__note {
  margin: 2.5rem 0 0;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
  color: var(--ink-muted);
  font-size: .875rem;
}

/* The rating block, closing the sheet.

   Two icon pills against the question, which is the shape the reference uses
   and the shape that survives the sheet narrowing: the ask wraps, the controls
   do not. `--verdict-control` sizes the pills from one place because "as tall
   as each other" is a relationship, not two numbers that happen to agree
   today -- the same reason the cookie notice declares its own. */
.article__verdict {
  --verdict-control: 2.5625rem;
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 1.5rem;
  align-items: center;
  justify-content: space-between;
  margin: 2.5rem 0 0;
  padding-top: 1.5rem;
  border-top: 1px solid var(--line);
}

.article__verdict-ask {
  /* Lets the column shrink instead of shoving the pills off the edge. */
  min-width: 0;
  flex: 1 1 14rem;
}

.article__verdict-title {
  margin: 0;
  color: var(--brand-ink);
  font-size: 1.0625rem;
  font-weight: 700;
}

.article__verdict-status,
.article__verdict-tally {
  margin: .25rem 0 0;
  color: var(--ink-muted);
  font-size: .8125rem;
}

/* Nothing said yet, so no gap where a line would be. */
.article__verdict-status:empty { display: none; }

/* On white, so these are the light-surface tones and not the footer's
   on-dark set. */
.article__verdict-status[data-tone="good"]  { color: var(--brand-success); }
.article__verdict-status[data-tone="bad"]   { color: var(--brand-danger); }
.article__verdict-status[data-tone="quiet"] { color: var(--ink-muted); }

.article__verdict-form {
  display: flex;
  gap: .625rem;
  /* Never the half that collapses. */
  flex: 0 0 auto;
}

.article__verdict-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 5.25rem;
  min-height: var(--verdict-control);
  padding: .5rem 1.125rem;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background-color: var(--surface-raised);
  color: var(--ink-secondary);
  font-size: 1.0625rem;
  cursor: pointer;
  transition: border-color var(--dur-fast) ease, background-color var(--dur-fast) ease, color var(--dur-fast) ease;
}

/* Green up, red down -- the only two controls in the app whose meaning is
   their direction, so they are the only ones that take the success and danger
   colours for something other than an outcome.

   Both get the same weight of colour, deliberately. Tinting only the thumbs-up
   would make the pair read as one button we want pressed and one we tolerate,
   and a rating that leans on the reader is not a rating.

   Hung off --up and --down rather than the js- hooks beside them: nothing in
   this file styles a js- class, because those name behaviour and are the ones
   a script is free to move. */
.article__verdict-button--up.article__verdict-button--chosen {
  border-color: var(--success-tint-edge);
  background-color: var(--success-tint);
  color: var(--brand-success);
}

.article__verdict-button--down.article__verdict-button--chosen {
  border-color: var(--danger-tint-edge);
  background-color: var(--danger-tint);
  color: var(--brand-danger);
}

/* After the chosen pair on purpose: they share a specificity, so this is what
   lets a hover still answer on the button already picked -- the edge darkens
   from the tint to the full colour and the fill stays put. */
@media (hover: hover) and (pointer: fine) {
  .article__verdict-button--up:hover {
    border-color: var(--brand-success);
    background-color: var(--success-tint);
    color: var(--brand-success);
  }

  .article__verdict-button--down:hover {
    border-color: var(--brand-danger);
    background-color: var(--danger-tint);
    color: var(--brand-danger);
  }
}

@media (max-width: 47.9375em) {
  /* The ask above, the pills below and full width: at this measure the two
     halves side by side leave the question on three lines. */
  .article__verdict { gap: 1rem; }
  .article__verdict-ask { flex-basis: 100%; }
  .article__verdict-form { flex: 1 1 100%; }
  .article__verdict-button { flex: 1 1 0; }
}

.article__aside {
  max-width: var(--content-column);
  padding: 1.5rem 2.25rem 1.75rem;
  background-color: var(--surface-raised);
  border-radius: 1rem;
}

.article__aside-title {
  margin: 0 0 1rem;
  font-size: 1.125rem;
  font-weight: 700;
}

/* The hub. One card per article, and the card carries the summary: the line is
   the half that tells somebody which of five pages they want, and a list of
   five titles alone makes them open two to find out. */
.article-list {
  display: grid;
  gap: .75rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.article-card {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  padding: 1rem 1.125rem;
  background-color: var(--surface-sunken);
  border-radius: .75rem;
  text-decoration: none;
  transition: background-color var(--dur-fast) ease;
}

.article-card__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 2.25rem;
  height: 2.25rem;
  color: var(--on-brand);
  font-size: .9375rem;
  background-color: var(--cat-blue);
  border-radius: 50%;
}

.article-card__text {
  display: grid;
  gap: .1875rem;
  min-width: 0;
}

.article-card__title {
  color: var(--brand-ink);
  font-size: 1rem;
  font-weight: 700;
}

.article-card__summary {
  color: var(--ink-secondary);
  font-size: .9375rem;
  line-height: 1.5;
}

@media (hover: hover) {
  .article-card:hover { background-color: var(--line); }
}

/* The help hub: a heading, a rail, and a stack of cards.

   No band, unlike every article under it -- see the note in
   help/index.html.twig. What that buys is the cards being the first thing on
   the page after one line of heading, which is what a page of nine choices
   wants. The old hub was one sheet holding five cards with a summary each; the
   summary moved up to the group, so what is left per article is a row, and a
   row wants a target the width of the card rather than a card of its own. */
.help {
  padding-bottom: 3rem;
}

.help-intro {
  /* Generous on both sides. The heading is the widest thing on the page and
     the cards below it are the busiest, so the two need to be told apart by
     space rather than by anything drawn. */
  padding: 2.5rem 0 3.5rem;
  text-align: center;
}

.help-intro__title {
  /* Wider than the measure the cards use, and centred, so it reads as a
     statement rather than as the first line of something. */
  max-width: 24ch;
  margin: 0 auto;
  color: var(--brand-ink);
  font-size: 2.5rem;
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -.01em;
}

.help-intro__pill {
  display: inline-block;
  margin: .875rem 0 0;
  padding: .4375rem 1.125rem;
  color: var(--on-brand);
  font-size: 1.0625rem;
  font-weight: 700;
  background-color: var(--brand-accent-fill);
  border-radius: 999px;
}

.help-layout {
  display: grid;
  /* Wider than --content-column on purpose: that measure is for prose, and
     this column holds rows. The rail is fixed and the cards take the rest, so
     the cards keep the reading width they had while the rail sits outside it. */
  grid-template-columns: minmax(0, 14.5rem) minmax(0, var(--content-column));
  gap: 1.5rem;
  align-items: start;
  justify-content: center;
}

.help-rail {
  /* Sticky rather than scrolling with the cards: the whole point of a rail is
     to still be there when you are three cards down. Offset by the header,
     which is sticky on pages carrying a `.hero` -- this one does not, so the
     offset is only the breathing room the cards get. */
  position: sticky;
  top: 1.5rem;
  padding: .5rem 0 0;
}

/* No card behind it. On a band page the rail would have had to be one to sit
   on navy; on the page background it is a list of links, and a white panel
   around three of them would weigh more than the cards it points at. */
.help-rail__title {
  margin: 0 0 .75rem;
  padding-left: .875rem;
  color: var(--ink-secondary);
  font-size: .8125rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
}

.help-rail__list {
  display: grid;
  gap: .125rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.help-rail__link {
  display: block;
  /* The left edge carries the current marker, so every link is indented by it
     whether it is current or not -- otherwise the text steps sideways as you
     scroll, which is more distracting than the marker is useful. */
  padding: .4375rem .5rem .4375rem .875rem;
  border-left: 2px solid transparent;
  color: var(--ink-secondary);
  font-size: .9375rem;
  line-height: 1.4;
  text-decoration: none;
  transition: color var(--dur-fast) ease, border-color var(--dur-fast) ease;
}

.help-rail__link[aria-current="true"] {
  border-left-color: var(--brand-accent);
  color: var(--brand-ink);
  font-weight: 700;
}

@media (hover: hover) {
  .help-rail__link:hover { color: var(--brand-ink); }
}

.help-groups {
  display: grid;
  gap: 1.5rem;
  min-width: 0;
}

/* The icon column, named because the rows below the heading line up with the
   far side of it rather than with the card's own edge -- see
   .help-group__list. Change either value and the alignment follows.

   Declared for both cards that draw a group: the hub's own, and the article's
   aside where it carries the same partial. On the modifier and not on
   .article__aside itself, because a legal document's aside is that class too
   and has no icon to clear. */
.help-group,
.article__aside--group {
  --help-group-icon: 2.5rem;
  --help-group-icon-gap: 1rem;
  --help-group-indent: calc(var(--help-group-icon) + var(--help-group-icon-gap));
}

.help-group {
  padding: 1.75rem 2rem 1.25rem;
  background-color: var(--surface-raised);
  border-radius: 1rem;
  /* Room above a card followed to from the rail, so its heading does not land
     hard against the top of the viewport. */
  scroll-margin-top: 1.5rem;
}

/* No focus ring on the section itself. It is only focusable so a rail link
   moves the cursor as well as the viewport; drawing a ring around a whole card
   would say "you have selected this", which is not what happened. The heading
   inside it is what the reader is looking at. */
.help-group:focus { outline: none; }

.help-group__head {
  display: flex;
  /* Centred where there is no sentence under the heading, which is the
     article's card: an icon hung from the top of a single line sits above it.
     The hub's heading block is two lines, so `start` is what that wants and
     the rule below restores it. */
  align-items: center;
  gap: var(--help-group-icon-gap);
  margin-bottom: 1.25rem;
}

.help-group .help-group__head {
  align-items: flex-start;
}

.help-group__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: var(--help-group-icon);
  height: var(--help-group-icon);
  color: var(--on-brand);
  font-size: 1rem;
  border-radius: 50%;
  /* The fallback matters: `accent` is a string in a row, so a value the
     stylesheet has never heard of has to draw something rather than a
     transparent hole where an icon should be. */
  background-color: var(--cat-blue);
}

/* The palette is the one the place rails already use, so a category cannot
   introduce a sixth colour by having a column typed into it. */
.help-group__icon--green  { background-color: var(--cat-green); }
.help-group__icon--blue   { background-color: var(--cat-blue); }
.help-group__icon--orange { background-color: var(--cat-orange); }
.help-group__icon--violet { background-color: var(--cat-violet); }
.help-group__icon--pink   { background-color: var(--cat-pink); }

.help-group__heading {
  min-width: 0;
}

.help-group__title {
  margin: 0;
  color: var(--brand-ink);
  font-size: 1.375rem;
  font-weight: 700;
  line-height: 1.25;
}

.help-group__summary {
  margin: .3125rem 0 0;
  color: var(--ink-secondary);
  font-size: .9375rem;
  line-height: 1.5;
}

/* The rows are shared with the article page's own group card, which borrows
   this list and everything under it. Only the card around them differs: there
   it is the article's aside, sized and titled as an aside. */
.help-group__list {
  /* Indented past the icon on the hub, so a row starts where the heading above
     it starts, and the rules go with it -- a hairline running out to the
     card's edge under a heading that begins 56px in reads as two columns
     rather than one list. Nought by default, because a card with no icon has
     no column to clear. */
  margin: 0 0 0 var(--help-group-indent, 0);
  padding: 0;
  list-style: none;
}

/* A rule between rows and not under the last one, so the card does not finish
   on a line with nothing below it. */
.help-group__item + .help-group__item {
  border-top: 1px solid var(--line);
}

.help-group__link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  /* Negative side margins so the row's hover fills the card's padding: a row
     that highlights short of its own edge looks like a mistake. */
  margin: 0 -.75rem;
  padding: .9375rem .75rem;
  border-radius: .5rem;
  color: var(--brand-ink);
  font-size: 1rem;
  text-decoration: none;
  transition: background-color var(--dur-fast) ease;
}

.help-group__label {
  min-width: 0;
}

.help-group__chevron {
  flex: 0 0 auto;
  color: var(--brand-accent);
  font-size: .8125rem;
  transition: transform var(--dur-fast) ease;
}

@media (hover: hover) {
  .help-group__link:hover { background-color: var(--surface-sunken); }
  /* A short nudge rather than a slide. The row is already highlighted, so this
     only has to say which way it goes. */
  .help-group__link:hover .help-group__chevron { transform: translateX(2px); }
}

.help-group__link:active {
  background-color: var(--line);
}

@media (max-width: 61.9375em) {
  /* The rail stops being a rail before the cards get narrow enough to hurt:
     it becomes a row of the same links above them, still sticky-free, still
     the same table of contents. */
  .help-layout {
    grid-template-columns: minmax(0, var(--content-column));
  }

  .help-rail {
    position: static;
    padding: 0;
  }

  .help-rail__title { padding-left: 0; }

  .help-rail__list {
    display: flex;
    flex-wrap: wrap;
    gap: .25rem .5rem;
  }

  .help-rail__link {
    padding: .375rem .625rem;
    border-left: 0;
    border-radius: 999px;
    background-color: var(--surface-sunken);
  }

  .help-rail__link[aria-current="true"] {
    color: var(--on-brand);
    background-color: var(--brand-accent-fill);
  }
}

@media (max-width: 47.9375em) {
  .help-intro { padding: .25rem 0 1.5rem; }
  .help-intro__title { font-size: 1.875rem; }
  .help-intro__pill { font-size: .9375rem; }
  .help-group { padding: 1.25rem 1.25rem 1rem; }
  .help-group__title { font-size: 1.1875rem; }
}

@media (max-width: 47.9375em) {
  .article__sheet,
  .article__aside {
    padding-right: 1.25rem;
    padding-left: 1.25rem;
  }
}


/* ==========================================================================
   #DIRECTORY
   ==========================================================================
   A few hundred named places, arranged to be found in. Shared by the city list
   and, next, the country list -- both are read the same way. */

.page-list { padding: 2rem 0 4rem; }

/* A page's title and the line under it, where the page is neither prose nor a
   directory. Those two carry their own header inside their sheet; a page whose
   content is cards has nowhere to put one, so /my/bookings and /my/saved had
   been borrowing Bootstrap's `display-4` and `.lead`. Measured, that was 56px
   at weight 300 against the 28-32px at 700 every other title in the app uses --
   the largest and lightest heading on the site, on the only two pages nobody
   had given a header of their own.

   The type is .directory__title's, because these pages are its kin: a title, a
   line saying what the list is, and then the list. Less room under it than the
   48px `py-5` left, which was air sized for a heading half as big again. */
.page-head {
  padding: 3rem 0 2rem;
}

.page-head__title {
  margin: 0 0 .375rem;
  font-size: 2rem;
  font-weight: 700;
}

.page-head__lead {
  margin: 0;
  color: var(--ink-muted);
  font-size: 1.0625rem;
}

.directory {
  padding: 2rem 2.25rem;
  background-color: var(--surface-raised);
  border-radius: 1rem;
}

.directory__title { margin: 0 0 .375rem; font-size: 2rem; font-weight: 700; }

.directory__lead {
  margin: 0 0 1.5rem;
  color: var(--ink-muted);
  font-size: 1.0625rem;
}

/* ---------- The filter ---------- */

.directory__search {
  position: relative;
  max-width: 26rem;
  margin-bottom: 1.5rem;
}

.directory__search-icon {
  position: absolute;
  top: 50%;
  left: 1rem;
  transform: translateY(-50%);
  color: var(--ink-muted);
  font-size: .9375rem;
  /* The input is the target; the icon is decoration in front of it. */
  pointer-events: none;
}

/* Every property restated: the reset at the top of this file strips borders
   from inputs and sets a global 18px, so nothing here is inherited. */
.directory__search-input {
  width: 100%;
  height: 3rem;
  padding: 0 1rem 0 2.75rem;
  color: var(--ink-body);
  font-size: 1rem;
  background-color: var(--surface);
  border: 1px solid transparent;
  border-radius: .75rem;
}

.directory__search-input::placeholder { color: var(--ink-muted); }

.directory__search-input:focus-visible {
  background-color: var(--surface-raised);
  border-color: var(--brand-accent);
  outline: 2px solid var(--brand-tint-edge);
  outline-offset: 1px;
}

/* ---------- The alphabet ---------- */

.directory__alphabet {
  display: flex;
  flex-wrap: wrap;
  gap: .25rem;
  margin-bottom: 1.5rem;
}

.directory__letter {
  display: flex;
  align-items: center;
  justify-content: center;
  /* Square and finger-sized. A row of single characters is the smallest target
     on the site otherwise. */
  min-width: 2.25rem;
  height: 2.25rem;
  padding: 0 .375rem;
  color: var(--ink-secondary);
  font-size: .9375rem;
  font-weight: 600;
  text-decoration: none;
  border-radius: .5rem;
  transition: background-color var(--dur-fast) ease, color var(--dur-fast) ease;
}

@media (hover: hover) and (pointer: fine) {
  .directory__letter:hover { background-color: var(--surface); }
}

.directory__letter.is-current {
  color: var(--on-brand);
  background-color: var(--brand-ink-chip);
}

/* ---------- The groups ---------- */

.directory__group {
  padding-top: 1.5rem;
  /* Clear of the heading when the alphabet jumps to it. */
  scroll-margin-top: 1.5rem;
}

/* One letter at a time, starting with the first.

   With no script running this is done entirely in CSS: the first group shows,
   and the alphabet's anchors are real anchors, so following one makes that
   group the :target and the default steps aside. That is why the groups are
   not rendered hidden -- a server-side default of "only A" would leave the
   other twenty-five letters pointing at sections nothing could reveal.

   Once the script is running it owns which group is visible and says so with
   the hidden attribute; these two rules hand it the decision. */
.directory__group { display: none; }
.directory__group:first-of-type { display: block; }
.directory__groups:has(:target) .directory__group:first-of-type { display: none; }
.directory__group:target { display: block; }

.directory.is-scripted .directory__group { display: block; }
.directory.is-scripted .directory__group[hidden] { display: none; }

.directory__group-title {
  margin: 0 0 .75rem;
  font-size: 1.5rem;
  font-weight: 700;
}

/* Columns rather than a grid: the names run down each one and a group of three
   does not leave a row of empty cells behind it. */
.directory__list {
  column-count: 4;
  column-gap: 1.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.directory__item[hidden] { display: none; }

/* The list is four columns, and a column break does not care that a name and
   the line under it are one thing: without this, "Heathrow" sat in column one
   and "London, United Kingdom" in column two. Harmless while every item was a
   single line, which is why it was not needed until airports arrived. */
.directory__item { break-inside: avoid; }

.directory__link {
  display: block;
  padding: .25rem 0;
  color: var(--brand-accent);
  font-size: .9375rem;
  text-decoration: none;
}

@media (hover: hover) and (pointer: fine) {
  .directory__link:hover { color: var(--brand-accent-700); text-decoration: underline; }
}

/* A second line under the name, for a caller whose names do not explain
   themselves. Cities and countries need none -- "Toronto" is Toronto. Airports
   do: Heathrow, Gatwick and Stansted are all London and none of them says so. */
.directory__note {
  display: block;
  padding-bottom: .25rem;
  color: var(--ink-muted);
  font-size: .8125rem;
}

.directory__item:has(.directory__note) .directory__link { padding-bottom: 0; }

.directory__empty {
  padding: 2rem 0;
  color: var(--ink-muted);
  font-size: 1rem;
}

@media (max-width: 1279.98px) { .directory__list { column-count: 3; } }
@media (max-width: 991.98px)  { .directory__list { column-count: 2; } }
@media (max-width: 575.98px) {
  .directory { padding: 1.25rem; }
  .directory__list { column-count: 1; }
}

/* ==========================================================================
   #AIRSIDE
   ==========================================================================
   The travel section, as against the help pages about booking here. A list of
   posts and one post, so there is less here than `.help-*` needs: no rail, no
   groups, no rating.

   Card sizes follow the help hub's scale rather than inventing a second one.
   ========================================================================== */

.airside-intro {
  /* The help hub's spacing exactly. Two sibling sections that sat differently
     on the page would read as two designs. */
  padding: 2.5rem 0 3.5rem;
  text-align: center;
}

.airside-intro__title {
  margin: 0 auto;
  color: var(--brand-ink);
  font-size: 2.5rem;
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -.01em;
}

.airside-intro__tagline {
  /* Not a pill, unlike the help hub's. The help pill carries a qualifier that
     finishes the heading's sentence; this line carries the meaning of a word
     most readers do not know, so it wants to read as prose rather than as a
     label. */
  max-width: 28ch;
  margin: .75rem auto 0;
  color: var(--ink-secondary);
  font-size: 1.125rem;
}

.airside-empty {
  padding: 3rem 0 4rem;
  color: var(--ink-muted);
  text-align: center;
}

.airside-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
  gap: 1.5rem;
  margin: 0 0 4rem;
  padding: 0;
  list-style: none;
}

/* The card standing in the middle of a post, naming another one.
 *
 * Tinted rather than white, because it sits inside the sheet and a white box
 * on white is a box nobody sees. It is the one thing in a body that is not the
 * author talking, so it should read as an interruption -- politely.
 */
.airside-inline {
  margin: 2.5rem 0;
}

.airside-inline__link {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1rem 1.25rem;
  text-decoration: none;
  background-color: var(--brand-tint);
  border-radius: var(--bs-border-radius-lg);
  transition: background-color var(--dur-fast) ease;
}

.airside-inline__image {
  /* Fixed and square: the card is a row, and a hero's 16:9 would set the
     height of everything beside it. */
  flex: 0 0 auto;
  width: 4.5rem;
  height: 4.5rem;
  object-fit: cover;
  border-radius: .5rem;
}

.airside-inline__text {
  min-width: 0;
}

.airside-inline__label {
  margin: 0 0 .25rem;
  color: var(--brand-accent);
  font-size: .75rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
}

.airside-inline__title {
  margin: 0;
  color: var(--brand-ink);
  font-size: 1.0625rem;
  font-weight: 700;
  line-height: 1.3;
}

@media (hover: hover) {
  .airside-inline__link:hover { background-color: var(--brand-tint-edge); }
  .airside-inline__link:hover .airside-inline__title { color: var(--brand-accent); }
}

/* The most-liked block: cards, deliberately unlike the rows above it. Two
   blocks that make different claims should not read as one list broken in
   half, which is the mistake the reference avoids by giving each its own
   shape. Narrower tracks than the hub's, so three fit across the card. */
.airside-list--liked {
  grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
  gap: 1rem;
  margin-bottom: 0;
}

.airside-list--liked .airside-card__summary {
  /* The card's line earns its place on the hub, where it says which of many
     posts you want. Three cards under a heading do not need it, and it would
     make this block taller than the post it follows. */
  display: none;
}

/* Related posts: rows, the shape help uses for the same job at the foot of an
   article. Cards were tried and nesting a bordered card inside a card is as
   fussy as it sounds -- and with one related post it left a lonely 239px card
   in a 748px block. */
.airside-related__list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.airside-related__link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  /* Negative side margins so the row's hover fills the card's padding: a row
     that highlights short of its own edge looks like a mistake. */
  margin: 0 -.75rem;
  padding: .9375rem .75rem;
  border-radius: .5rem;
  color: var(--brand-ink);
  font-size: 1rem;
  text-decoration: none;
  transition: background-color var(--dur-fast) ease;
}

.airside-related__label {
  min-width: 0;
}

.airside-related__chevron {
  flex: 0 0 auto;
  color: var(--brand-accent);
  font-size: .8125rem;
  transition: transform var(--dur-fast) ease;
}

@media (hover: hover) {
  .airside-related__link:hover { background-color: var(--surface-sunken); }
  /* A short nudge rather than a slide: the row is already highlighted, so this
     only has to say which way it goes. */
  .airside-related__link:hover .airside-related__chevron { transform: translateX(2px); }
}

.airside-related__link:active {
  background-color: var(--line);
}

.airside-card {
  background-color: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--bs-card-border-radius);
  overflow: hidden;
}

.airside-card__link {
  /* The whole card, so the target is the card and not the words in it. */
  display: flex;
  flex-direction: column;
  height: 100%;
  color: inherit;
  text-decoration: none;
}

.airside-card__link:hover .airside-card__title,
.airside-card__link:focus-visible .airside-card__title {
  color: var(--brand-accent);
}

.airside-card__image {
  /* A fixed ratio, because the images are committed by hand and nothing
     resizes them yet -- A8.2 owns that. Without this a tall photograph would
     set the height of its whole grid row. */
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

.airside-card__text {
  display: flex;
  flex: 1;
  flex-direction: column;
  padding: 1.25rem;
}

.airside-card__title {
  margin: 0;
  color: var(--brand-ink);
  font-size: 1.1875rem;
  font-weight: 700;
  line-height: 1.25;
}

.airside-card__summary {
  margin: .5rem 0 0;
  color: var(--ink-secondary);
  font-size: .9375rem;
  line-height: 1.5;
}

.airside-card__meta {
  /* Pushed to the bottom, so the date sits on one line across a row of cards
     whose summaries are different lengths. */
  display: flex;
  gap: .5rem;
  margin: 1rem 0 0;
  color: var(--ink-faint);
  font-size: .8125rem;
}

.airside-card__author::before {
  content: '·';
  margin-right: .5rem;
}

/* Bootstrap gives a blockquote nothing but margin, so an unstyled one is
   indistinguishable from a paragraph. The same rule the README already has --
   `.readme blockquote` -- but scoped to the shared article body, which is
   where help and Airside both print their prose and where neither had it. */
.article__body blockquote {
  padding: .25rem 0 .25rem 1rem;
  margin: 1.5rem 0;
  border-left: 3px solid var(--brand-tint-edge);
  color: var(--ink-secondary);
}

.article__body blockquote > :last-child {
  margin-bottom: 0;
}

/* A callout: the one thing in a post that is louder than the prose around it.
   `{.callout}` on its own line above a blockquote is how an author writes one,
   because a body cannot contain a `<div>` -- see View\Markdown.

   Heavier than the plain blockquote on both counts, rule and weight, so the
   two are told apart at a glance rather than by reading them. */
.article__body blockquote.callout {
  padding: 1rem 0 1rem 1.25rem;
  border-left-width: 4px;
  border-left-color: var(--brand-accent);
  color: var(--brand-ink);
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.5;
}

/* A term and its explanation, for the "what you need" lists a post wants and
   bullets do not serve. */
.article__body dl {
  margin: 1.5rem 0;
}

.article__body dt {
  color: var(--brand-ink);
  font-weight: 700;
}

.article__body dd {
  margin: .25rem 0 1rem;
  color: var(--ink-secondary);
}

/* Footnotes, which is where a date-sensitive travel fact belongs. Smaller and
   set off by a rule, because by the time a reader is here they have finished
   the post. */
.article__body .footnotes {
  margin-top: 2.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
  color: var(--ink-secondary);
  font-size: .875rem;
}

.article__body .footnotes hr {
  display: none;
}

/* The tags that close a post.
 *
 * Inside the sheet and after the byline. They began in the partial's `aside`
 * block, which renders outside it, so three lozenges floated in the gutter
 * between the article and the footer and read as page furniture.
 *
 * Outlined rather than filled. The site's existing pill -- `.help-intro__pill`
 * -- is solid accent, which is right for one pill carrying a page's subtitle
 * and wrong for three sitting under a byline, where it would out-shout the
 * heading. The border says "control", the hover says "link".
 */
/* The author, in a card of its own. No border and no rule: it is
   `.article__aside`, so the card edge and the grid's gap do the dividing that
   a hairline was doing badly. */
.airside-author {
  display: flex;
  align-items: center;
  gap: 1rem;
}

.airside-author__portrait {
  /* Fixed, because the two states have to occupy the same box: a photograph
     and a pair of letters that stand in for one. */
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  overflow: hidden;
  background-color: var(--brand-tint);
  border-radius: 50%;
}

.airside-author__picture {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.airside-author__initials {
  color: var(--brand-accent);
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: .02em;
}

.airside-author__name {
  margin: 0;
  color: var(--brand-ink);
  font-size: .9375rem;
  font-weight: 700;
}

.airside-author__date {
  margin: .125rem 0 0;
  color: var(--ink-muted);
  font-size: .875rem;
}

.airside-tags__label {
  /* Small, quiet, and not optional: without it a reader meets three words in
     boxes and has to work out what they are.

     `--ink-muted` and not `--ink-faint`, which is 3.41:1 on white and fails
     the 4.5:1 this needs at 12px. Measured, after writing `--ink-faint` first
     because it looked right. */
  margin: 0 0 .75rem;
  color: var(--ink-muted);
  font-size: .75rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
}

.airside-tags__list {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.airside-tags__pill {
  display: inline-block;
  /* 2.25rem tall at this padding, which clears the 24px minimum target size
     without making three tags look like three buttons. */
  padding: .4375rem 1rem;
  color: var(--brand-ink);
  font-size: .875rem;
  font-weight: 600;
  line-height: 1.3;
  text-decoration: none;
  background-color: var(--surface-raised);
  /* The border is what says "control", so it has to clear 3:1 against the
     sheet. Every `--line-*` token here fails that -- 1.25, 1.41 and 1.64 to
     one -- which is correct for what they are for, a hairline between blocks,
     and wrong for the edge of something clickable. `--ink-faint` is 3.41. */
  border: 1px solid var(--ink-faint);
  border-radius: 999px;
  transition: color .15s ease, border-color .15s ease, background-color .15s ease;
}

.airside-tags__pill:hover,
.airside-tags__pill:focus-visible {
  color: var(--brand-accent);
  background-color: var(--brand-tint);
  border-color: var(--brand-accent);
}

@media (prefers-reduced-motion: reduce) {
  .airside-tags__pill { transition: none; }
}

/* The contents list, which only appears where a post writes `[TOC]` -- so a
   short post has none rather than a two-line list of its own headings. */
.article__body > .table-of-contents {
  padding: 1.25rem 1.5rem;
  margin: 0 0 2.5rem;
  background-color: var(--surface-sunken);
  border-radius: var(--bs-border-radius-lg);
  list-style: none;
}

.article__body .table-of-contents ul {
  margin: .375rem 0 0;
  padding-left: 1.125rem;
  list-style: none;
}

.article__body .table-of-contents li {
  margin: .3125rem 0;
  font-size: .9375rem;
  line-height: 1.4;
}

.article__body .table-of-contents > li > a {
  font-weight: 600;
}

.article__body .table-of-contents a {
  color: var(--brand-ink);
  text-decoration: none;
}

.article__body .table-of-contents a:hover,
.article__body .table-of-contents a:focus-visible {
  color: var(--brand-accent);
  text-decoration: underline;
}

/* The anchor a heading carries so the contents list has somewhere to land. It
   renders no symbol on purpose -- the id is the whole point, and a row of
   pilcrows down the page is decoration for a feature nobody clicks. */
.article__body .heading-anchor {
  display: inline;
  scroll-margin-top: 1.5rem;
}

/* A figure: an image that stands on its own, with an optional caption. The
   body's measure is the prose measure, so a figure runs the full width of it
   and the caption sits under the image rather than beside it. */
.article__body figure {
  margin: 2rem 0;
}

.article__body figure img,
.article__body p > img {
  /* Never wider than the column, and always the shape the file is -- the
     width and height attributes come off the file itself, so `height: auto`
     is what keeps the two from fighting. */
  width: 100%;
  height: auto;
  border-radius: var(--bs-border-radius-lg);
}

.article__body figcaption {
  margin-top: .625rem;
  color: var(--ink-muted);
  font-size: .875rem;
  line-height: 1.45;
}

.airside-post__hero {
  /* Inside `.article__body`, so it takes the prose measure rather than the
     full width, and sits above the first paragraph. */
  width: 100%;
  /* Required by the `width`/`height` attributes, not decoration. The browser
     reads the two as an aspect ratio and reserves the box before the file
     lands; `width: 100%` alone would then stretch it to the attribute's
     height. `auto` lets the ratio do the work. */
  height: auto;
  margin: 0 0 1.5rem;
  border-radius: var(--bs-border-radius-lg);
}

@media (max-width: 575.98px) {
  .airside-intro { padding: 1.75rem 0 2.5rem; }
  .airside-intro__title { font-size: 2rem; }
  .airside-list { gap: 1.25rem; }
}
