Ctrl K
ComunidadGuías…

src/app/globals.css

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
@import "tailwindcss";
@config "../../tailwind.config.mts";

/* A finger is driving. The same query as `hooks/useCoarsePointer.ts`, so a
   device that gets the OS date wheels also gets the finger-sized controls. See
   "On touch, every control is 44px to a finger" in DECISIONS.md. */
@custom-variant touch (@media (hover: none) and (pointer: coarse));

/* A 44px hit area centred on a control smaller than that, drawn by `::after` so
   the control keeps its look. The element must be positioned, since the
   pseudo-element is placed against it. Used as `touch:tap-target`. */
@utility tap-target {
  &::after {
    content: "";
    position: absolute;
    inset-block: min(0px, calc((100% - 2.75rem) / 2));
    inset-inline: min(0px, calc((100% - 2.75rem) / 2));
  }
}

/* Lifts text, or an icon, off whatever a backdrop shows beneath it, with a
   glow in `--backdrop-fade`: the colour a hero's map fades to, and a card's
   glass is tinted. The icon's is a filter because `text-shadow` stops at an
   SVG, and keeps to two close layers: more of them, or wider ones, drew each
   layer's edge as a visible ring - `drop-shadow`s chain, each blurring the
   last one's 8-bit output. The text's `text-shadow` layers do not chain. */
@utility glow-text {
  text-shadow:
    0 0 1px var(--backdrop-fade),
    0 0 8px var(--backdrop-fade);
}

@utility glow-icon {
  filter: drop-shadow(0 0 2px var(--backdrop-fade))
    drop-shadow(0 0 5px var(--backdrop-fade));
}

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;

    --card: 0 0% 100%;
    --card-foreground: 222.2 84% 4.9%;

    --popover: 0 0% 100%;
    --popover-foreground: 222.2 84% 4.9%;

    /* A deliberately dark overlay surface, for things that float *above* a card
       and have to read as detached from it - currently the air-consumption
       chart's hover card. Not --popover, which is the same color as --card in
       both themes (white here, 13% lightness under .dark) and so leans entirely
       on its 1px border to separate from whatever it's sitting on. Over a chart
       that's not enough: the card overlaps its own data points, and a surface
       that matches the panel underneath reads as part of it.

       Dark in *both* themes rather than inverting per theme - a near-black chip
       is the conventional chart tooltip, and it keeps the same weight against a
       white card as it does against a dark one. */
    --tooltip: 222 47% 11%;
    --tooltip-foreground: 210 40% 98%;

    --primary: 240 2% 15%;
    --primary-foreground: 0 0% 100%;

    --secondary: 210 40% 96%;
    --secondary-foreground: 222.2 84% 4.9%;

    --muted: 210 40% 96%;
    /* Darkened from shadcn's default 46.9%. At that lightness it is 4.65:1 on
       --background (barely AA) and only 4.34:1 on --muted, so every muted line on a
       tinted surface - the footer, a card's description - failed. 40% clears AA on
       both (6.0:1 and 5.4:1) and is still visibly secondary to --foreground. */
    --muted-foreground: 215 16% 40%;

    --accent: 210 40% 96%;
    --accent-foreground: 222.2 84% 4.9%;

    /* Errors and destructive actions are coral, not red. Hue 10 sits six degrees
       off the brand --coral's 16, in the same warm family as every other accent
       here; shadcn's default saturated hue-0 red would be the one colour in the
       app belonging to no other token, and against a palette built on coral and
       teal it reads as imported rather than chosen.

       Also darker than shadcn's red - 42% against 60.2% - and that is not a side
       effect of the hue. `text-destructive` is real body text in `FormMessage`,
       and 60.2% is 3.76:1 on --background: an AA failure on every validation
       message in the app. Hue 10 at 42% is 5.5:1 there, and 4.6:1 over its own
       /10 tint, which is the surface `StatusMessage` and the delete-account panel
       put it on. */
    --destructive: 10 88% 42%;
    --destructive-foreground: 210 40% 98%;

    /* Same coral, darkened until the label on it clears WCAG AA - 5.6:1 under the
       light theme's near-white --destructive-foreground, 5.9:1 under the dark
       theme's pure white. --destructive is dark enough in *this* theme to carry
       white itself; the separate token is for the dark theme, where --destructive
       has to be light enough to read as text on a near-black background and so
       cannot sit under a white label. It is the palette's only per-role split: the
       brand accents are a single hue each, and the destructive pair is the
       exception because here the colour carries meaning rather than brand.
       --destructive for text, icons and tints; --destructive-solid for anything
       filled. */
    --destructive-solid: 10 88% 40%;

    /* The counterpart to --destructive, for a confirmation that has to persist in
       place rather than in a toast (see `StatusMessage`). A token rather than a
       green-600/green-400 pair per component, so the pair is tuned once and stays
       consistent with --destructive's weight. Darker than the obvious green so
       it clears AA against --background as body text. */
    --success: 142 72% 29%;
    --success-foreground: 0 0% 100%;

    /* Between --success and --destructive in weight: something to take seriously
       that is not a failure: caution text, the course status badge, and the
       terms page's safety notice, which is deliberately *not* one of the
       `bg-muted` informational callouts. A dive log telling people it is not a
       source of safety advice should not look like a footnote. */
    --warning: 32 92% 27%;
    --warning-foreground: 0 0% 100%;

    --border: 214.3 31.8% 91.4%;
    --input: 214.3 31.8% 91.4%;
    --ring: 240 2% 15%;

    /* The two brand accents, and each is exactly its CSS named colour: --coral is
       `coral` (#FF7F50) and --teal is `teal` (#008080). That is the whole
       definition, deliberately - one hue per accent, not per-role variants such as
       --coral-solid, --coral-text and --teal-solid, each a contrast-tuned darkening
       for one job (a filled button, text on a light background), which leave every
       call site picking between coral-ish classes. Anything coral is `coral`;
       anything teal is `teal`.

       Both are constant across light/dark - not redeclared under .dark - so a
       brand colour reads the same in both themes.

       65.7%, not 66%: the rounded value renders #FF8052, two units off coral on
       two channels. Imperceptible on its own and not imperceptible next to
       `icon.svg`, which hard-codes #FF7F50 for the favicon. 180 100% 25% needs no
       such correction; it lands on #008080 exactly.

       The cost is real and accepted. It falls differently on each of the three
       tuned variants, so the call sites are worth naming rather than summarising
       - DECISIONS.md carries the same three lists, and they are meant to stay
       identical:

       --coral-text (6.1:1 on white -> 2.5:1, light theme only; the dark theme
       loses nothing, since --coral-text would resolve to the brand coral there):
       the landing hero's accent word, the header's nav items active in both the
       desktop and the mobile menu and on hover in the desktop one, the Home page
       stat cell's label on group-hover, and the species card's hover border.

       --coral-solid (5.1:1 under white -> 2.5:1, in *both* themes - neither token
       is redeclared under .dark, so fill and white label are theme-constant and
       the pairing never involves --background): the sign-in button in the
       header, and AuthForm's submit button. This is where the single hue costs
       the dark theme, and it is easy to reason about backwards from
       --coral-text's dark behaviour, which does not apply here.

       --teal-solid (5.8:1 under white -> 4.8:1, both themes, still clearing AA):
       Button's `default` variant, so every filled button in the app, and the
       calendar's selected day.

       The exception that keeps its tuned pair is the one where colour carries
       meaning rather than brand: errors and destructive actions,
       --destructive / --destructive-solid above. */
    --coral: 16 100% 65.7%;
    --teal: 180 100% 25%;

    /* Labels for anything *filled* with a brand accent, which the gear service
       badge is the first thing to do - everywhere else coral and teal are strokes
       or glyphs. Both are pure white, which puts the three service chips on one
       label colour in the dark theme and within a shade of it in the light one,
       where --destructive-foreground is the near-white 210 40% 98% rather than
       #FFF. Close enough to read as one treatment; not close enough to call them
       the same value.

       Neither is a second coral or a second teal, and so neither is what the
       collapse above removed: they are the colour that sits *on* the accent, the
       pairing --destructive, --success, --warning and every other filled token
       here already have. Constant across themes because the accents are - a
       per-theme pair would have nothing to adapt to.

       --coral-foreground is where this palette pays for the uniformity, and the
       cost is on the record rather than hidden: white on #FF7F50 is 2.50:1, under
       the 4.5:1 AA asks of the label, where a near-black would have been 7.23:1.
       Teal is the dark half of the pair and has no such problem - 4.77:1 under
       white, and only 3.8:1 under the near-black, so it wanted white anyway.
       See "The service scale is three brand fills, and `--warning`'s amber
       would be a fourth accent" in DECISIONS.md. */
    --coral-foreground: 0 0% 100%;
    --teal-foreground: 0 0% 100%;

    /* Third chart accent, for the dive profile's tank-pressure curve. A neutral
       grey beside depth's teal and temperature's coral: the gauge reading is the
       supporting line on that plot, and grey keeps it there while staying
       separable from both by hue.

       Declared here and deliberately *not* redeclared under .dark, the same
       property that makes coral and teal hold their contrast in both themes:
       52% lightness is 3.9:1 against a white card and 4.2:1 against the dark
       theme's 13% one, and still 3.3:1 under a gas-switch marker's resting 0.9
       opacity on white. A per-theme pair would have to be tuned twice and would
       drift. */
    --pressure: 220 6% 52%;

    /* Fourth chart accent, for the dive profile's deco ceiling. Red, which is
       what every dive computer and every other dive log uses for the ceiling,
       and the one mark on this chart that is a *limit* rather than a
       measurement - the region it bounds is water the diver may not enter.

       Declared once and deliberately not redeclared under .dark, the same call
       --pressure, --teal and --coral make. 55% lightness is 4.3:1 against the
       light card and 3.8:1 against the dark theme's 13% one, so it clears the
       3:1 WCAG asks of a graphical object in both - which is the property a
       per-theme pair would have to be re-tuned twice to keep, and would drift.
       That is also why it isn't --destructive, which is a light/dark pair tuned
       for text and for white-on-fill, and not for a stroke over a chart.

       This sits closer to --coral's hue 16 than a chart accent normally should,
       and temperature is the channel most likely to be plotted beside a
       ceiling. What separates them is lightness and saturation rather than hue
       - coral is a pale salmon at 66%, this is a saturated red at 55% - plus
       the fact that the ceiling is the only mark on the chart drawn as a dashed
       line over a shaded region. Colour is doing the least of the work here,
       which is the condition that makes the overlap acceptable. */
    --ceiling: 0 80% 55%;

    /* The heart-rate row's accent, the first row under the depth plot. A rose,
       because red is what every watch and dive computer draws a heart rate in,
       and pulled to 345 so it is not the ceiling's 0: the two never share a
       plot, and the ceiling is dashed over a shaded region, so as with --ceiling
       and --coral colour is doing the least of the work. Alone on its row, so it
       has nothing there to be told apart from.

       Declared once and never redeclared under .dark, like the accents below,
       and measured the same way: 4.18:1 against the light card, 3.90:1 against
       the dark one. */
    --heart-rate: 345 80% 55%;

    /* Six accents for the rows under the depth plot that a dive computer
       computes rather than measures: the no-decompression clock, the time to surface, the
       ppO₂ it calculated, the CNS clock and the two gradient factors.

       Declared once and never redeclared under .dark, the same call --pressure,
       --ceiling, --teal and --coral make - and each is picked so that it clears
       the 3:1 WCAG asks of a graphical object against **both** cards, which is
       the property a per-theme pair would have to be re-tuned twice to keep.
       Measured against the light card's white and the dark theme's 13% one:

         --ndl                       3.35:1 light, 4.86:1 dark
         --tts                       3.48:1 light, 4.67:1 dark
         --ppo2                      4.02:1 light, 4.05:1 dark
         --cns                       3.79:1 light, 4.30:1 dark
         --gradient-factor           4.17:1 light, 3.91:1 dark
         --surface-gradient-factor   4.29:1 light, 3.79:1 dark

       **What has to be separable is what shares a row**, and the panel is one
       row per scale: heart rate, no-deco time, time to surface, ppO2, and the
       three percentages. Within that last row the hues are 90 degrees apart or
       more. Across rows they are not always - --surface-gradient-factor's 230
       sits 25 degrees from --ppo2's 205 - and that is the same trade --ceiling
       already makes at 16 degrees from --coral, for the same reason: two curves
       that never share a plot are told apart by which plot they are in, and the
       legend and the crosshair both name them in words. Colour is doing the
       least of the work.

       Hues chosen around the three already spoken for (0 ceiling, 16 coral,
       180 teal; --pressure is grey) rather than through them, so no new accent
       has to be told apart from an old one by lightness alone. */
    --ndl: 150 65% 38%;
    --tts: 35 95% 40%;
    --ppo2: 205 85% 45%;
    --cns: 300 60% 55%;
    --gradient-factor: 95 55% 35%;
    --surface-gradient-factor: 230 70% 62%;

    /* A cylinder's gas, in the US medical-gas colours dive shops borrow:
       oxygen green, helium brown. See `components/icons/tank-icon.tsx`. */
    --gas-oxygen: 144 100% 39%;
    --gas-helium: 25 100% 37%;

    /* A backdrop card's own colour and its hover, a step off the page's either
       way - see `BackdropCard`. */
    --backdrop-surface: 240 4% 95.5%;
    --backdrop-surface-hover: 240 4% 92.5%;

    /* The shipped basemaps' water and the colour of the sea names drawn on it,
       for anything standing in for a map of open water - a dive card with no
       place yet. Whole colours rather than HSL triplets so they are exactly
       what `scripts/generate-basemaps.mjs` writes; `basemap.test.ts` fails when
       the two part. */
    --map-water: rgb(163 206 205);
    --map-water-foreground: rgb(37 96 96);

    --radius: 0.5rem;

    /* The part of the page the browser is actually showing, for overlays that
       have to fit inside it - currently `ui/dialog.tsx`. `useVisualViewport`
       overwrites both from `window.visualViewport` while a dialog is open, and
       these are what a browser that never fires those events falls back to.

       `svh` rather than `vh`, because they differ on exactly the device this
       matters on: iOS measures `vh` against the viewport Safari would have with
       its toolbars retracted, so `100vh` is taller than the screen for as long
       as they are up. The `@supports` below keeps `vh` for anything that has
       never heard of `svh`, where an unsubstitutable variable would otherwise
       drop the whole declaration that used it. */
    --visual-viewport-height: 100vh;
    --visual-viewport-top: 0px;
    /* The anchor of the backdrops behind a dialog and a picker sheet, in
       document coordinates. Unlike the pair above, this fallback is a floor
       rather than a correct value: page scroll has nothing to do with
       `visualViewport` support, so a browser without the API that opens either
       partway down a long page anchors the backdrop at the top of the document
       and misses the screen. Left as is deliberately - every browser the app
       supports has had `visualViewport` for years, so the only thing a better
       fallback would protect is a consumer that does not exist. Worth knowing
       before reusing this variable for anything else. */
    --visual-viewport-doc-top: 0px;
  }

  @supports (height: 100svh) {
    :root {
      --visual-viewport-height: 100svh;
    }
  }

  /* macOS Dark Mode's actual system colors: windowBackgroundColor /
     systemGray6 for surfaces (a very slightly cool/blue-gray, not a
     perfectly neutral gray), labelColor for text, and macOS's "Graphite"
     system accent color (a desaturated gray, not blue) for primary/ring.
     Destructive is the exception: it is in the coral family, as in the light
     theme, for the reasons on the token itself below. */
  .dark {
    --background: 240 4% 9%;
    --foreground: 240 6% 93%;

    --card: 240 4% 13%;
    --card-foreground: 240 6% 93%;

    --popover: 240 4% 13%;
    --popover-foreground: 240 6% 93%;

    /* Darker than both --card (13%) and --background (9%), so the hover card
       still reads as floating above the panel rather than cut out of it. Kept
       on the same cool gray hue as the rest of the dark palette rather than
       reusing the light theme's slightly blue near-black. */
    --tooltip: 240 6% 4%;
    --tooltip-foreground: 240 6% 93%;

    --primary: 240 2% 45%;
    --primary-foreground: 0 0% 100%;

    /* Nine points above --card rather than shadcn's three. --secondary's whole job
       is to fill a chip, and every one of them in this app sits on a card: at 16% a
       secondary badge was 1.2:1 against the surface behind it and simply wasn't
       there. The light theme gets away with a 4-point gap because its --secondary
       carries a 40%-saturation blue tint that separates it by hue; this palette is
       deliberately near-neutral, so lightness is the only channel available.
       Deliberately NOT merged with --muted, which stays at 16% - that one is a
       large-area wash (the footer, callouts) where 22% would read as a panel. */
    --secondary: 240 4% 22%;
    --secondary-foreground: 240 6% 93%;

    --muted: 240 4% 16%;
    --muted-foreground: 240 3% 60%;

    --accent: 240 4% 18%;
    --accent-foreground: 240 6% 93%;

    /* Same hue 10 as the light theme, lightened for the dark surface exactly as
       --success and --warning are: 6.9:1 on --background and 6.2:1 on --card, which
       it needs because this is the theme where --destructive is read as text. */
    --destructive: 10 100% 68%;
    --destructive-foreground: 0 0% 100%;

    /* Constant across themes, like the brand accents: it is only ever under white
       label text, so it has nothing to adapt to. */
    --destructive-solid: 10 88% 40%;

    /* Lightened for the dark surface, exactly as --destructive is. */
    --success: 142 65% 55%;
    --success-foreground: 240 4% 9%;

    /* Lightened for the dark surface, exactly as --success is. */
    --warning: 38 95% 62%;
    --warning-foreground: 240 4% 9%;

    --border: 240 4% 18%;
    --input: 240 4% 18%;
    --ring: 240 2% 55%;

    /* Helium a step lighter on the dark surface. */
    --gas-helium: 30 100% 44%;

    --backdrop-surface: var(--card);
    --backdrop-surface-hover: var(--muted);

    --map-water: rgb(68 108 107);
    --map-water-foreground: rgb(178 226 226);
  }
}

@layer base {
  * {
    @apply border-border;
  }

  /* The header is `sticky top-0`, so it is painted over the top of whatever an
     anchor jump scrolls to: clicking Features in the nav put `#features` at
     `top: 0` and hid its first header-height behind it. `scroll-padding-top`
     moves the scroll container's snap line down instead of offsetting each
     target, so it covers `#features`, `#self-hosting`, `#get-started` and any
     anchor added later, plus the `scrollIntoView` of a focused element, which
     had the same problem. On the scroll container, hence `html` not `body`.

     The value has to be the header's height *exactly*, and the first attempt at
     this got the safe direction backwards. It used 5rem against a 69px header,
     reasoning that spare clearance is harmless - but the extra 11px is a strip
     of whatever sits immediately *above* the target, and above `#self-hosting`
     is the full-bleed `bg-primary` band, so jumping there showed an 11px black
     bar (grey in dark mode) welded under the header. Overshooting is the bad
     direction. Undershooting is the harmless one: every section on that page
     carries its own `py-20`, so a pixel or two short only ever eats empty
     padding.

     So this is derived from the header rather than measured off a screenshot:
     `py-4` twice, the `size="sm"` action button at `h-9`, and the 1px
     `border-b`. On touch it is `py-3` twice and the button's 44px floor, which
     is the same sum. It therefore tracks the root font size the same way the
     header does, which a px literal would not.

     `header.tsx`'s mobile menu and its backdrop start at this line too, so the
     header above them stays undimmed and its burger stays tappable.

     The combobox listboxes are unaffected either way: they `scrollIntoView({
     block: "nearest" })` inside their own overflow container, which has its own
     (unset) scroll-padding that a rule on `html` cannot reach. */
  :root {
    --header-height: calc(4.25rem + 1px);
  }

  /* A card section's padding: `p-6`, and 12px on a phone, where the page's
     gutter and the gap between cards are 10px. A variable rather than a
     `max-sm:` class beside `p-6`, so a caller's `p-0` or `print:p-0` replaces
     it outright - tailwind-merge only drops a class of the same variant. */
  :root {
    --card-pad: 1.5rem;
    /* Half of what a `size="sm"` button's 36px exceeds a card title's line by:
       24px, and 18px on a phone. On touch the button is 44px. */
    --card-title-lift: 0.375rem;
    @variant touch {
      --card-title-lift: 0.625rem;
    }
  }

  @media (width < 40rem) {
    :root {
      --card-pad: 0.75rem;
      --card-title-lift: 0.5625rem;
      @variant touch {
        --card-title-lift: 0.8125rem;
      }
    }
  }

  html {
    scroll-padding-top: var(--header-height);
  }

  /* Where a row pinned to an edge draws its surface: faded rather than popped
     in, which matters most when one comes back as the keyboard closes. The dive
     form's row carries it on itself, a dialog's rows on a `::before` that
     reaches a gap's height into the content - there at rest too, so that the
     fade has a start. */
  [data-dive-form-actions] {
    @apply transition-[background-color,box-shadow,backdrop-filter] duration-150 motion-reduce:transition-none;
  }

  /* The padding under the dive form's buttons. Installed to a phone's home
     screen, the page runs down into the screen's rounded corners, which a
     browser's toolbar otherwise covers, so there the pinned buttons sit as
     high as a dialog's do - its 1rem inset plus its footer's `pb-6` - or
     above the home indicator's inset, where one is reported, and the action
     button clears the curve. The same at rest, so nothing moves as the row
     sticks. */
  :root {
    --dive-form-actions-pb: 0.75rem;
    @media (display-mode: standalone) {
      --dive-form-actions-pb: max(2.5rem, env(safe-area-inset-bottom, 0px));
    }
  }

  [data-dive-form-actions] {
    padding-bottom: var(--dive-form-actions-pb);
  }

  /* The box each surface - and the header's cross - is placed against, pinned
     or not. */
  [data-dialog-header],
  [data-dialog-footer] {
    position: relative;
  }

  [data-dialog-header]::before,
  [data-dialog-footer]::before {
    content: "";
    position: absolute;
    z-index: -1;
    pointer-events: none;
    opacity: 0;
    @apply bg-background/90 transition-opacity duration-150 motion-reduce:transition-none;
  }

  [data-dialog-header]::before {
    inset: 0 0 -1rem;
    @apply shadow-[inset_0_-1px_0_hsl(var(--border))];
  }

  /* Above the buttons, so the rule does not sit on them, without the footer
     itself growing at rest. */
  [data-dialog-footer]::before {
    inset: -1rem 0 0;
    @apply shadow-[inset_0_1px_0_hsl(var(--border))];
  }

  /* The dive form's Cancel/Save row sticks to the foot of the screen, and a
     dialog's title row and footer to its top and foot while the content scrolls
     between them - unless the visible height is short, which with the keyboard
     up it is: then each stays where it rests and the field being typed into
     keeps the room. `useShortViewport` sets the attribute, and takes it off
     only once the viewport has settled.

     A row resting off screen reads as stuck to its observer, so everything
     keyed on `data-stuck` sits under the same gate.

     The dialog offsets are measured from inside the content's `p-6`, so they
     cancel that padding to reach the edge, and go two pixels past it: pinned,
     and at rest at that end of the dialog - the same place, each row bleeding
     into the padding - then differ by a hidden sliver the row's observer can
     see. */
  :root:not([data-visual-viewport-short]) {
    [data-dive-form-actions] {
      position: sticky;
      bottom: 0;
    }

    /* Stuck, it covers what scrolls under it. A shadow rather than a border, as
       on a stuck section heading: the rule takes no height, so nothing moves as
       the row sticks. */
    [data-dive-form-actions][data-stuck] {
      @apply bg-background/90 shadow-[inset_0_1px_0_hsl(var(--border))] backdrop-blur;
    }

    /* The page's bottom scroll padding while that row is pinned: a field focused
       below it - the first refused one after a failed save - stops above the
       row rather than behind it. The row's height: `pt-3`, a default button's
       `h-10` (44px on touch) and the padding under it. Unlike at the top,
       overshooting costs nothing here. The refusal line the row can carry is
       not counted: a resolver's refusal, the usual one, never shows there. */
    &:has([data-dive-form-actions][data-stuck]) {
      scroll-padding-bottom: calc(3.25rem + var(--dive-form-actions-pb));
      @variant touch {
        scroll-padding-bottom: calc(3.5rem + var(--dive-form-actions-pb));
      }
    }

    [data-dialog-header] {
      position: sticky;
      top: calc(-1.5rem - 2px);
    }

    [data-dialog-footer] {
      position: sticky;
      bottom: calc(-1.5rem - 2px);
    }

    [data-dialog-header][data-stuck]::before,
    [data-dialog-footer][data-stuck]::before {
      opacity: 1;
      @apply backdrop-blur;
    }

    /* The header's height, which `DialogHeader` measures, and that gap. */
    [role="dialog"]:has([data-dialog-header][data-stuck]) {
      scroll-padding-top: calc(var(--dialog-header-height, 0px) + 1rem);
    }

    /* The footer's height, which `DialogFooter` measures, and that gap. */
    [role="dialog"]:has([data-dialog-footer][data-stuck]) {
      scroll-padding-bottom: calc(var(--dialog-footer-height, 0px) + 1rem);
    }
  }

  body {
    @apply bg-background text-foreground;
  }

  /* Tailwind v4's Preflight dropped v3's `button { cursor: pointer }` to match
     the browser default. Restored here, because without it the app is visibly
     inconsistent along a line that has nothing to do with what a control does:
     A `Button` rendered as a `Link` gets a hand (it renders an anchor, and
     the UA styles those) while every other button - the same component, the
     same appearance - gets an arrow. The alternative is patching `cursor-pointer`
     onto components one at a time, which is what was happening.

     Base UI's menu and listbox items are unaffected: they carry an explicit
     `cursor-default` (native OS menus don't use a hand), and a utility always
     outranks this layer. Same for `disabled:cursor-not-allowed` on inputs. */
  button:not(:disabled),
  [role="button"]:not(:disabled),
  select:not(:disabled),
  input:where([type="checkbox"], [type="file"]):not(:disabled) {
    cursor: pointer;
  }

  /* No double-tap zoom on a control, so tapping one twice in quick succession -
     a period arrow, a map zoom button, a star - is two taps. Pinch still zooms.
     A drag handle's inline `touch-action: none` (`useDragSort`) outranks this. */
  a,
  button,
  [role="button"],
  input,
  label,
  select,
  summary,
  textarea {
    touch-action: manipulation;
  }

  /* Every date field is a native one on a touch device
     (`hooks/useCoarsePointer.ts`), and WebKit sizes those to the widget rather
     than to the box it was given: on an iPhone the date fields in a dialog come
     out wider than the text fields above them and overflow it. Dropping the
     native appearance is what lets `Input`'s own `h-10 w-full` hold the row, and
     `min-width: 0` is the same fight one level out, where the widget's intrinsic
     width would stop a grid or flex cell narrowing to a phone's.

     Alignment and the height of an empty field need nothing here, and a rule
     for either is worth checking on a device before adding: Preflight's own
     `::-webkit-date-and-time-value { text-align: inherit; min-height: 1lh }`
     already covers both on iOS. */
  input[type="date"],
  input[type="time"] {
    appearance: none;
    min-width: 0;
  }
}

/* MapLibre's cooperative-gesture screen, hidden so the app can draw its own.

   `cooperativeGestures` is what gives the picker four behaviours at once - one
   finger scrolls the page, two drive the map, the wheel only zooms with
   ctrl/cmd, and `touchmove` is prevented from two touches up. It also renders a
   40%-black overlay of its own, with MapLibre's wording, on *every* blocked
   gesture. That last part is a regression rather than a feature here: a plain
   wheel over this map is the diver scrolling down the dialog towards Save, and
   flashing a dark panel over the map every time they do it is worse than the
   silence it replaces. So the screen is hidden and
   `components/map/map-picker.tsx` listens for `cooperativegestureprevented`
   instead, showing the app's own hint for the one case that needs explaining.

   Outside `@layer base`, and two class names deep, because `maplibre-gl.css` is
   imported from a client component's own chunk rather than from here - so its
   rules may arrive after these, and only specificity can be relied on.

   And a stylesheet rather than `map.cooperativeGestures.disable()`, which is the
   obvious way to be rid of the screen and takes the behaviour with it: its
   `_destroyUI()` removes `maplibregl-cooperative-gestures` from the canvas
   container, and that class is what MapLibre's own `touch-action: pan-x pan-y`
   rule keys off. Hiding one element cannot touch a class on another, which is
   exactly why it is the right instrument here. */
.maplibregl-map .maplibregl-cooperative-gesture-screen {
  display: none;
}

/* The hero's reef accent is a CC0 silhouette (see `NOTICE.md`), carried as a
   19 KB two-colour PNG rather than the 318 KB SVG it was traced into: the
   drawing's polyp stipple is thousands of tiny loops, which is why neither
   svgo nor WebP could shrink it, and why none of that detail survives being
   drawn at 25% opacity anyway.

   A mask rather than an `<img>`, so the shape takes its colour from a token and
   follows the theme the way the rest of the page does. `mask-mode: luminance`
   is required, not decorative: the file has no alpha channel - the shape is
   white on black - and `match-source` would read a missing alpha as fully
   opaque and paint the whole box.

   Nothing is hand-prefixed for it, deliberately. WebKit never shipped a
   `-webkit-mask-mode`: the legacy spelling is `-webkit-mask-source-type`, and
   the build emits that on its own - `@tailwindcss/postcss` runs lightningcss
   against Safari 16.4 and friends, which prefixes the whole `mask-*` block. A
   hand-written `-webkit-mask-mode` sat here until a reviewer checked the built
   CSS and found it passed through as an unknown declaration, doing nothing. */
.hero-reef {
  -webkit-mask-image: url("/reef-mask.png");
  mask-image: url("/reef-mask.png");
  mask-mode: luminance;
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  background-color: hsl(var(--teal));
  aspect-ratio: 3516 / 5556;
}