/*! Calculingo -- calculingo.app
 * Copyright (c) 2026 Andrei Suslov. All rights reserved.
 *
 * Proprietary and source-available. Delivered to your browser so the page can
 * be drawn; that is not a grant of any right to use, copy, adapt or host it.
 * Permission can be asked for: truvord@gmail.com
 */
/* /calc: one input, one answer. Deliberately spare; light and dark. */

/* Inter, served from here rather than from Google. The hosted version cost two
 * extra hosts on the critical path -- a render-blocking stylesheet from
 * fonts.googleapis.com whose only job was to name a file on fonts.gstatic.com,
 * which then needed its own DNS and TLS before a glyph could be asked for.
 * Measured cold, the stylesheet alone was ~345ms of that, and preconnect only
 * shortens the handshake, it does not remove the hop. Same-origin the font
 * rides the connection the page already has.
 *
 * Inter is a variable font, so the three weights the page uses are three
 * instances of one file, not three files: `font-weight: 100 900` hands the
 * whole wght axis to the browser and it interpolates 400/500/600 from that.
 *
 * Two faces because the split pays for itself. The latin subset is everything
 * the page normally draws. The currency file is the five symbols Intl can hand
 * back that latin does not carry -- the shekel, rupee, won, peso and dong; the
 * euro is in latin already. Google's own latin-ext covers those five at 85KB,
 * which is more than the rest of the app put together, so it is subsetted down
 * to the currency block at 8KB. The unicode-range means it is only ever
 * fetched by someone who asks about one of those currencies.
 *
 * The ?v= is the first eight hex of the file's own sha256, not the date the
 * rest of the site is versioned by. These two files are the one thing here
 * that can be cached forever and never bumped again, and a date would undo
 * that: every deploy would rename them and re-send 48KB that had not changed
 * a byte. Hashed, the name moves when and only when the glyphs do. It has to
 * be repeated on the preload in index.html -- a preload for a URL that is not
 * the URL @font-face later asks for is simply a second download. */
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("fonts/inter-latin.woff2?v=c9407645") 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: "Inter";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("fonts/inter-currency.woff2?v=bd8a360a") format("woff2");
  unicode-range: U+20A0-20AB, U+20AD-20C0;
}

*,
*::before,
*::after { margin: 0; padding: 0; box-sizing: border-box; }

:root {
  --bg: #ffffff;
  --text: #1a1a1a;
  --muted: #6b7280;
  --faint: #9ca3af;
  --line: #e5e7eb;
  --chip: #f9fafb;
  --font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  /* The gap above the input. Named because the motto has to fill it exactly:
     two copies of this clamp would drift apart the first time either moved,
     and the price of them disagreeing is the input changing place. */
  --calc-top: clamp(3rem, 12vh, 8rem);
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0f1115;
    --text: #f3f4f6;
    --muted: #9ca3af;
    --faint: #6b7280;
    --line: #262a33;
    --chip: #171a21;
  }
}

body {
  font-family: var(--font);
  background: var(--bg);
  color: var(--text);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  /* So the footer can be held at the bottom of a short page rather than
     floating halfway up one. dvh as well as vh: on a phone vh is the window
     with the address bar hidden, which it is not when the page first draws,
     so vh alone reserves space for furniture that is still on screen. */
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

/* The column above is a flex container, which makes every wrap in it a flex
   item, and an auto margin across the flex axis cancels the stretch that would
   otherwise have made the item as wide as the page. Without a width of its
   own each wrap fell back to the width of whatever was inside it: the nav was
   as wide as the word "calculingo", and the calculator was as wide as its
   longest saved query, so the whole app changed width between one visit and
   the next and there was no telling which width you would get.

   width: 100% gives the cross size the stretch was meant to give it. The
   max-width then caps it and the auto margins centre it, which is what both
   were always there to do. */
.wrap {
  width: 100%;
  max-width: 720px;
  margin: 0 auto;
  padding: 0 1.5rem;
}

.sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* ===== NAV ===== */
.nav {
  border-bottom: 1px solid var(--line);
  height: 64px;
  display: flex;
  align-items: center;
}

.nav__logo {
  color: inherit;
  text-decoration: none;
  font-weight: 600;
  font-size: 0.9375rem;
}

/* ===== CALC ===== */
.calc {
  padding-top: var(--calc-top);
  padding-bottom: 4rem;
}

#q {
  display: block;
  width: 100%;
  font: inherit;
  font-size: clamp(1.5rem, 5vw, 2rem);
  font-weight: 500;
  letter-spacing: -0.01em;
  color: inherit;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  padding: 0 0 0.75rem;
  outline: none;
}

#q::placeholder { color: var(--faint); }
#q:focus { border-bottom-color: var(--muted); }

/* Inline completion. What was typed is repeated here in transparent text, so
   the grey remainder starts exactly where the caret is whatever the font does
   with the letters before it: nothing is measured and nothing is inserted into
   the input. Every metric below has to match #q or the two drift apart. */
.field { position: relative; }

.ghost {
  position: absolute;
  inset: 0;
  overflow: hidden;
  white-space: pre;
  pointer-events: none;
  font: inherit;
  font-size: clamp(1.5rem, 5vw, 2rem);
  font-weight: 500;
  letter-spacing: -0.01em;
  padding: 0 0 0.75rem;
  border-bottom: 1px solid transparent;
  color: var(--faint);
}

.ghost span:first-child { color: transparent; }

/* A phone keyboard has no Tab key, so the grey text is its own tap target. */
.ghost span:last-child { pointer-events: auto; cursor: pointer; }
.ghost[hidden] { display: none; }

/* ...and while there is grey text in the line, the placeholder is not the
   thing to read. It only ever draws when the box is empty, and the box is
   empty at exactly one moment that matters here: the arrows have lifted the
   typed word out to make room for the offer. Both are grey, both start where
   the text starts, and the reader got the example printed through the middle
   of the suggestion. 27-page decides when, since only it knows the ghost is
   up. */
#q.hushed::placeholder { color: transparent; }

.out {
  display: block;
  margin-top: 2rem;
  font-variant-numeric: tabular-nums;
  min-height: 6rem;
}

/* "can't read that yet" / "Dec 2026 has only 31 days" */
.out--note { color: var(--faint); font-size: 0.9375rem; }

/* The same refusal, when it has a reading to offer underneath it. */
.out__why { color: var(--faint); font-size: 0.9375rem; }
.out--note .detail { color: var(--muted); margin-top: 0.375rem; }

/* Nearly every answer is short enough that none of this matters. The ones that
   are not are one unbroken run of digits with nowhere to wrap: "pi to 100
   places" is a hundred-character word, and a word is what the browser treats
   it as, so it ran off the right-hand side and took the whole document with
   it. The page itself gained a horizontal scrollbar, which moved the nav, the
   chips and the footer sideways as well, none of which were too wide for
   anything. break-word rather than anywhere: normal answers still wrap between
   words, and only a run that cannot fit a line on its own gets split inside. */
.head {
  font-size: clamp(2rem, 8vw, 3rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.2;
  overflow-wrap: break-word;
}

.detail {
  margin-top: 0.5rem;
  color: var(--muted);
  font-size: 0.9375rem;
}

/* The answer said in sentences, for the questions where a paragraph is what
   an almanac would print and a row of dot-separated fragments is not: an
   eclipse is read for the sense of it rather than scanned for a figure. Wider
   line height than the fragments above because it is prose and gets read left
   to right, and pre-line because a list of them is one paragraph each. */
.prose {
  margin-top: 0.75rem;
  max-width: 42em;
  line-height: 1.55;
  font-size: 1rem;
  white-space: pre-line;
}

/* The sum itself, in symbols and then with the numbers in it. Monospaced,
   because it is arithmetic being shown rather than prose being read, and the
   digits lining up is most of what makes it legible. */
.working {
  margin-top: 0.375rem;
  color: var(--muted);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.8125rem;
  line-height: 1.5;
  overflow-x: auto;
  white-space: nowrap;
}

/* Room to breathe after a paragraph. Against the fragment lines a quarter of
   a rem reads as the next line down, which is what it is; against prose it
   reads as a caption on the last sentence, which it is not. */
.prose + .echo {
  margin-top: 0.75rem;
}

.echo {
  margin-top: 0.25rem;
  color: var(--faint);
  font-size: 0.875rem;
}

/* The figure a sun answer draws sits between the answer and the history list,
   and everything about how it looks is in sun.css, which arrives with it. All
   this file does is reserve the place: the box is empty and hidden until an
   answer has a latitude in it, and a reader who never asks about the sun never
   loads a byte of the other sheet. */

/* ===== EXAMPLES ===== */
.hints {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 1rem;
}

.hints button {
  font: inherit;
  font-size: 0.8125rem;
  color: var(--muted);
  background: var(--chip);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.25rem 0.75rem;
  cursor: pointer;
}

.hints button:hover {
  color: var(--text);
  border-color: var(--muted);
}

/* ===== 404 ===== */
.notfound {
  padding-top: clamp(3rem, 12vh, 8rem);
  padding-bottom: 4rem;
  color: var(--muted);
}

.notfound a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.notfound a:hover { color: var(--text); }

/* ===== HISTORY ===== */
/* The query scrolls under a fade; short queries are unaffected because the
   mask only covers empty space, so no JS overflow check is needed. */
.log { list-style: none; margin-top: 2rem; }

.log li {
  display: flex;
  align-items: baseline;
  gap: 1rem;
  padding: 0.4rem 0;
  border-top: 1px solid var(--line);
  font-size: 0.9375rem;
}

.log__q {
  flex: 1;
  min-width: 0;
  overflow-x: auto;
  white-space: nowrap;
  text-align: left;
  font: inherit;
  color: var(--muted);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  scrollbar-width: none;
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 1.5rem), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 1.5rem), transparent);
}

.log__q::-webkit-scrollbar { display: none; }
.log__q:hover { color: var(--text); }

/* A row of the list is one line, and stays one line however long the answer
   is: a saved query is something to scan past, and a row that grew to five
   lines of digits would push everything under it off the screen to say one
   thing nobody is reading yet. So the answer scrolls inside itself instead,
   exactly as the query beside it does, with the same hidden scrollbar and the
   same fade off the right-hand edge to say there is more of it.

   The answer keeps its natural width and the query takes what is left, which
   is right for every answer that fits and was the whole of the old rule. What
   was missing is what happens when it does not fit: an item that may not
   shrink does not, so the row stayed its own width and the answer hung out of
   the side of it. It may now give way, up to a point -- max-width is what
   keeps a hundred digits from taking the row and leaving the query nothing to
   be recognised by, and it is also what lets the query go on growing, since an
   answer capped at part of the row leaves the rest as free space rather than a
   shortage. An answer with room is untouched by all of it. */
.log__a {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 60%;
  overflow-x: auto;
  white-space: nowrap;
  scrollbar-width: none;
  font-variant-numeric: tabular-nums;
}

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

/* The fade that says there is more of it off to the right, put on only where
   there is. The query beside it can wear one unconditionally because it is the
   half that takes all the leftover width, so its last 1.5rem is nearly always
   empty and the gradient falls on nothing. This one is only ever as wide as
   the answer in it, so an unconditional fade dimmed the end of every short
   answer that never overflowed anything: "12.4274 miles" lost its s. Which
   rows get it cannot be known from the stylesheet, so renderLog measures and
   says. */
.log__a--more {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 1.5rem), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 1.5rem), transparent);
}

.clear {
  font: inherit;
  font-size: 0.8125rem;
  color: var(--faint);
  background: none;
  border: 0;
  padding: 0.5rem 0;
  cursor: pointer;
}

.clear:hover { color: var(--text); }
.hints[hidden], .clear[hidden] { display: none; }

/* Answers copy to the clipboard on click; the dim is the receipt. */
.head, .log__a { cursor: pointer; }
.copied { opacity: 0.4; transition: opacity 120ms; }

/* ===== CURRENCY PICKER ===== */
/* No box: the list is an extension of the sentence, so it starts at the caret
   and carries no border, no card and no shadow. The current value is not in
   here at all, it is in the line itself, greyed. */
.picker {
  position: absolute;
  top: 100%;
  margin: 0.1em 0 0;
  padding: 0;
  list-style: none;
  z-index: 5;
  font-size: clamp(1.5rem, 5vw, 2rem);
  /* Five values are on show at a time and one of them is up in the line, so
     the box holds four. The rest are a scroll away, which is why the fade
     follows the window and not the list: whatever is at the top of the box
     reads first, whatever is at the bottom is on its way in. */
  max-height: calc(4 * 1.37em);
  overflow-y: auto;
  /* A row longer than the space left between the caret and the edge is read by
     scrolling it, and that is the only direction there is anything in: the
     list begins at the caret and runs right. Sideways it therefore neither
     bounces at its own start nor hands the swipe up to the page, which on a
     trackpad had a horizontal flick over the suggestions rocking the whole
     document. Down the list still stops at the list, as it always did. */
  overscroll-behavior-x: none;
  overscroll-behavior-y: contain;
  scrollbar-width: none;                 /* the arrows do the scrolling */
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 18%, rgba(0, 0, 0, 0.45) 100%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 18%, rgba(0, 0, 0, 0.45) 100%);
}

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

.picker[hidden] { display: none; }

.picker li {
  display: grid;
  grid-template-columns: minmax(4.5em, auto) 1fr auto;
  gap: 0.8em;
  align-items: baseline;
  line-height: 1.25;                     /* five of these make the window */
  padding: 0.06em 0;
  cursor: pointer;
  white-space: nowrap;
}

.picker__offer { color: var(--faint); }
.picker__alt { color: var(--line); font-size: 0.5em; }
.picker__flag { font-size: 0.62em; }

/* Pointing at a row lifts it out of the fade. */
.picker li:hover { opacity: 1; }
.picker li:hover .picker__offer { color: var(--text); }
.picker li:hover .picker__alt { color: var(--muted); }

/* ===== MOTTO ===== */
/* It says what the page is to somebody who has just arrived, and then it stops
 * saying it. Three things here are one decision -- that the box underneath it
 * must not move, ever.
 *
 * Only opacity animates, so the line keeps its space when it goes. Nothing
 * beneath a caret is allowed to jump three seconds after the page loaded.
 *
 * It does not sit above the gap between the header and the input, it is that
 * gap: the negative margin pulls it up over the padding it would otherwise
 * begin below, and its height hands that padding back. The space above the
 * input is the number it always was, so the input has not moved -- but the
 * text is centred in the whole span now instead of pressed against the box.
 *
 * And its height comes from --calc-top rather than from the text it holds, so
 * the size of this line is a decision about this line. Set it larger and the
 * input does not follow it down the page. */
.motto {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: calc(-1 * var(--calc-top));
  height: calc(var(--calc-top) + 2.75rem);
  text-align: center;
  color: var(--muted);
  font-size: clamp(1.25rem, 4vw, 1.5rem);
  transition: opacity .45s ease;
}
.motto[data-gone] { opacity: 0; }
@media (prefers-reduced-motion: reduce) { .motto { transition: none; } }

/* ===== FOOTER ===== */
/* No top margin of its own: every main above it ends in its own bottom
   padding, and two spacings stacked would put the rule further from the page
   than from the edge of the window. */
.foot {
  /* Pushed to the bottom when the page above does not reach there. On a long
     page this does nothing, the main having already filled the height. */
  margin-top: auto;
  border-top: 1px solid var(--line);
  padding: 1.25rem 0 2rem;
  font-size: 0.8125rem;
  color: var(--faint);
}

.foot .wrap {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
}

.foot a {
  color: inherit;
  text-decoration: none;
}

.foot a:hover,
.foot a:focus-visible { color: var(--text); }

/* The page the reader is already on stays in the row, so the three do not
   shuffle about between pages, and is darkened rather than dropped: a link
   that goes nowhere new is still the answer to "where am I". */
.foot [aria-current="page"] { color: var(--muted); }

/* Held at the foot of the window rather than at the foot of the document.
   The releases page is several screens of history and the version number sat
   at the end of all of them, which is a strange place to keep the one line a
   reader may have come for: the footer is where the version lives and where
   the other pages are reached from, and neither should cost a scroll.

   Sticky rather than fixed, so it still occupies its own space and the last
   release is not printed underneath it. On a page shorter than the window
   this does nothing at all -- the footer was already in view, and `margin-top:
   auto` above had already pushed it down.

   Prose pages only. The calculator has a suggestion list that opens downward
   out of the input and would spend the rest of its life arguing with this for
   the same few pixels. */
.page ~ .foot {
  position: sticky;
  bottom: 0;
  /* Opaque, or the history scrolls through it. */
  background: var(--bg);
}

/* ===== PROSE PAGES ===== */
.page {
  padding-top: clamp(2.5rem, 8vh, 5rem);
  padding-bottom: 4rem;
}

.page h1 {
  font-size: 1.375rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  margin-bottom: 1.75rem;
}

.page p {
  margin-bottom: 1rem;
  color: var(--muted);
  max-width: 60ch;
}

.page a {
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.page a:hover { color: var(--muted); }

/* A key on a keyboard, drawn rather than spelled. The glyph is a path and not
   a character: ⏎ and ↵ are missing from enough fonts to fall back to a box,
   and an emoji would be a picture of a key rather than the key.

   The size and the colours live on the element as attributes, not here. This
   rule draws the keycap around a glyph that has already drawn itself, so a
   stale copy of this file costs the reader a border and nothing more. Set
   here, a stylesheet that had not arrived yet left an 11-pixel arrow filling
   the width of the page in solid black, which is what an SVG with no width
   and no fill does. */
.key {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.15em 0.35em;
  margin: 0 0.05em;
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 0.3rem;
  background: var(--chip);
  color: var(--muted);
  vertical-align: -0.15em;
}

.key svg { display: block; }

/* ===== RELEASES ===== */
.rel { margin-bottom: 2.25rem; }

.rel__when {
  font-size: 0.8125rem;
  color: var(--faint);
  margin-bottom: 0.5rem;
}

.rel h2 {
  font-size: 0.9375rem;
  font-weight: 600;
  margin-bottom: 0.75rem;
}

.rel ul { list-style: none; max-width: 60ch; }

/* The marker is text rather than a list-style so it takes the faint colour
   without the row taking it too. */
.rel li {
  color: var(--muted);
  padding-left: 1.1rem;
  text-indent: -1.1rem;
  margin-bottom: 0.4rem;
}

.rel li::before {
  content: "·";
  color: var(--faint);
  padding-right: 0.6rem;
}

/* The version, at the far end of the footer row, so it is on every page
   without being the first thing on any of them. It is there to be quoted in
   a bug report, which means it has to be visible and does not have to be
   prominent. */
.foot__v {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
}

.rel__v {
  display: inline-block;
  min-width: 3.5rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* A change shown rather than described. A sentence explaining that a summer
   day was being labelled as crossing a midnight it never crossed takes longer
   to read than the two answers side by side, and is less convincing: the
   query and what it used to say and what it says now is the whole of the
   claim, and a reader can check it in the box above.

   The marker and the hanging indent come off, the query supplying its own
   left edge. */
.rel li.rel__eg {
  padding-left: 0;
  text-indent: 0;
  margin-bottom: 0.9rem;
}
.rel li.rel__eg::before { content: none; }

.rel__q {
  display: block;
  color: var(--text);
  margin-bottom: 0.15rem;
}

/* Was and now, one under the other, so the eye drops rather than scans. The
   labels are in the stylesheet because they are furniture: every one of these
   rows says the same two words and none of them is content. */
.rel__was,
.rel__now { display: block; padding-left: 1.1rem; }

.rel__was::before,
.rel__now::before {
  display: inline-block;
  min-width: 3.2rem;
  color: var(--faint);
}
.rel__was::before { content: "was"; }
.rel__now::before { content: "now"; }
.rel__was { color: var(--faint); }
.rel__now { color: var(--muted); }
