/* ==================================================================== */
/* ND — SHARED BASE                                                      */
/*                                                                       */
/* The rules every page needs: the reset, the document defaults, the     */
/* type scale, the primary button, and the page shell.                   */
/*                                                                       */
/* ORDER MATTERS. This file is linked FIRST on every page, ahead of that */
/* page's own stylesheet, which is then free to override anything here.  */
/* Four pages depend on exactly that: legal, payment and registration    */
/* restate the typography they used to inherit from <body>, because the  */
/* type scale below would otherwise turn their body copy into display    */
/* type. Move the link and those pages change.                           */
/*                                                                       */
/* Colour never appears here as a literal — it comes from CSS/theme.css, */
/* imported below, so one link pulls in both the base and the tokens.    */
/*                                                                       */
/* This file replaced ten byte-identical copies of itself: the seven     */
/* shared-*.css files under 005-011, and the base block that had been    */
/* pasted into 002/003/004's index-*.css alongside 500-odd lines of      */
/* landing-page CSS those pages never used. Anything added here lands    */
/* on every page at once, so keep it to what is genuinely universal —    */
/* if only one page needs a rule, it belongs in that page's stylesheet.  */
/* ==================================================================== */

@import url("/CSS/theme.css");

* {
  box-sizing: border-box;
}

/* ---------------- Typeface ---------------- */
/* The face the whole site is set in, in one place, because "let's try the other
   one" should be one line rather than sixty-two.
 
   Roboto first. Inter behind it as the fallback rather than a bare
   system-ui, so a failed webfont lands on the face this was set in until now
   instead of on whatever the OS keeps — and so switching back is done by
   swapping the two names here.
 
   h1 and h2 in this file, and the handful of display rules elsewhere, name
   Roboto outright and always have. They look identical while this token is
   Roboto, and they are what keeps those headings on Roboto if it goes back to
   Inter. Leave them. */
:root {
  --nd-font: "Roboto", "Inter", system-ui, sans-serif;
}

/* ---------------- Tracking ---------------- */
/* Inter's default spacing is drawn for text around 16–20px, and it wants help
   on both sides of that: tightened as it gets bigger, opened up as it gets
   smaller. Rasmus Andersson ships a table of exactly this with the typeface —
   it is the one thing Inter asks of you.

   Half of it was already done here. Every display size carries its own
   negative tracking (h2 -1.28px, h3 -0.88px, the index hero -0.64px), and the
   small caps-ish labels their own positive (0.06em–0.08em on the section
   heads). What nobody had set was the middle, which is most of the site: a
   chat bubble at 15px, a form field at 14px, a member row, a work title, a
   button — all running at the metrics of text half again their size, which is
   why it reads a little cramped without ever looking obviously wrong.

   0.01em is what that table asks for around 13–14px, and it is deliberately
   small: this is the difference between text that sits comfortably and text
   somebody has spaced out on purpose. Everything above with tracking of its
   own overrides it, so the headings are untouched.

   It survives the change of face rather than being retuned with it. Roboto
   wants tracking too and slightly more of it — Material's own scale puts 14px
   at 0.25px, which is nearer 0.018em — so 0.01em is the conservative end for
   Roboto rather than the wrong answer, and turning it up is one number here.
   Kept where it was on purpose: the face is the thing being judged, and moving
   two variables at once means learning nothing about either. */
:root {
  --nd-tracking: 0.01em;
}

body {
  margin: 0;
  padding: 0;
  background-color: var(--nd-bg);
  color: var(--nd-text);
  font-family: var(--nd-font);
  /* Inherited, so it reaches the whole site from one line — including inputs
     and buttons, which take inherited properties even though the UA stylesheet
     overrides `font` on them (which is why they each say `font-family:
     inherit` and none of them will need to say this). */
  letter-spacing: var(--nd-tracking);
}

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

/* ---------------- Scrollbars ---------------- */
/* One look for every scrollable thing on the site — the page itself, modals,
   dropdowns, chat rails — instead of the OS's grey slab.

   Two mechanisms because no engine takes both: `scrollbar-*` is the standard
   (Firefox) and ::-webkit-* is everything Chromium and Safari. Both are set
   here, and both inherit, so a scroll container anywhere on the site picks the
   look up without opting in. A page that wants no scrollbar at all still says
   so on its own element (the chat rails do) and wins on specificity.

   The thumb is drawn narrower than the track it runs in: the padding-box clip
   turns the 3px transparent border into breathing room, so the bar reads as a
   thin line with margins rather than a slab wedged against the content. */
html {
  scrollbar-width: thin;
  scrollbar-color: var(--nd-scrollbar-thumb) transparent;
}

::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

::-webkit-scrollbar-track,
::-webkit-scrollbar-corner {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background-color: var(--nd-scrollbar-thumb);
  border: 3px solid transparent;
  background-clip: padding-box;
  border-radius: 999px;
}

::-webkit-scrollbar-thumb:hover {
  background-color: var(--nd-scrollbar-thumb-hover);
}

/* ---------------- Images ---------------- */
/* An image fits its frame. It is never stretched to fill it and never cropped
   to cover it — whatever was uploaded is shown whole, whether that frame is a
   row's thumbnail, a grid card, the Best Work panel or the window a work opens
   into. The frames on this site are fixed shapes and the work that goes in
   them is every shape a design can be, so anything else silently edits
   somebody's work: `fill`, the CSS default, squashes it, and `cover` cuts the
   edges off.

   It goes here, on the element, rather than into each frame's own rule: the
   ones that had it were the ones somebody remembered, and a new frame
   shouldn't have to remember. Any rule that genuinely wants a crop — a photo
   filling a circle — still says `object-fit: cover` in its own stylesheet and
   still wins, being both later and more specific. */
img {
  object-fit: contain;
}

/* ---------------- File inputs ---------------- */
/* The one control on the site the browser draws itself: a grey slab reading
   "Choose Files", in the operating system's font, sitting inside a modal made
   of lilac and glass. Its label and its behaviour still belong to the browser
   — only the button can be reached, through ::file-selector-button — but that
   is the part anyone looks at, so it's dressed like the inputs it stands next
   to: same tinted fill, same purple rim, same radius.

   On the element rather than on a class, because "everywhere it appears" is
   the requirement, and a file input added to some other page later would
   otherwise be back to the grey slab.

   The -webkit- rule below is the same declarations again, deliberately not
   merged into one selector list: an engine that doesn't know one of the two
   pseudo-elements drops the whole rule, and then one of the browsers gets
   nothing. Two rules, so each engine takes the one it understands. */
input[type="file"] {
  color: var(--nd-text-quiet);
  font-family: var(--nd-font);
  font-size: 14px;
}

input[type="file"]::file-selector-button {
  margin-right: 12px;
  padding: 10px 18px;
  background-color: var(--nd-surface-tinted);
  color: var(--nd-text);
  border: 1px solid var(--nd-purple);
  border-radius: 10px;
  font-family: var(--nd-font);
  font-size: 14px;
  cursor: pointer;
  transition: background-color 0.2s ease, border-color 0.2s ease;
}

input[type="file"]::-webkit-file-upload-button {
  margin-right: 12px;
  padding: 10px 18px;
  background-color: var(--nd-surface-tinted);
  color: var(--nd-text);
  border: 1px solid var(--nd-purple);
  border-radius: 10px;
  font-family: var(--nd-font);
  font-size: 14px;
  cursor: pointer;
  transition: background-color 0.2s ease, border-color 0.2s ease;
}

input[type="file"]::file-selector-button:hover {
  background-color: var(--nd-panel-hover);
  border-color: var(--nd-action);
}

input[type="file"]::-webkit-file-upload-button:hover {
  background-color: var(--nd-panel-hover);
  border-color: var(--nd-action);
}

/* ---------------- Type scale ---------------- */
h1, h2, h3, h4 {
  margin: 0;
}

h1 {
  font-family: "Roboto", sans-serif;
  font-weight: 700;
  font-size: 40px;
  line-height: 44px;
  letter-spacing: 0.4px;
}

h2 {
  font-family: "Roboto", sans-serif;
  font-weight: 400;
  font-size: 32px;
  line-height: 44.8px;
  letter-spacing: -1.28px;
}

h3 {
  font-family: var(--nd-font);
  font-weight: 300;
  font-size: 22px;
  line-height: 30.8px;
  letter-spacing: -0.88px;
}

h4 {
  font-family: var(--nd-font);
  font-weight: 400;
  font-size: 16px;
  line-height: 22.4px;
  letter-spacing: -0.64px;
}

/* ---------------- Primary button ---------------- */
.btn-primary {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 175px;
  height: 50px;
  background-color: var(--nd-purple);
  border-radius: 10px;
  border: none;
  color: var(--nd-on-accent);
  font-family: var(--nd-font);
  font-size: 16px;
  font-weight: 400;
  cursor: pointer;
  transition: filter 0.2s ease, transform 0.2s ease;
}

.btn-primary:hover {
  filter: brightness(1.1);
  transform: translateY(-2px);
}

/* ---------------- Page shell ---------------- */
/* Clears the fixed 60px navbar. Only pages carrying the navbar use it. */
.page-body {
  padding-top: 60px;
  min-height: 100vh;
}

/* ---------------- Page loading overlay ---------------- */
/* Raised by JS/page-loading.js while a page is waiting on the API. Every page
   that fetches shows the same thing, so it lives here rather than being
   rebuilt per page.

   It starts below the navbar rather than at the top of the viewport, and that
   is the whole point of it: the page underneath is half-built and must not be
   touched, but the navbar is not the page — its links, the language picker and
   the way out are all still legitimate while the content loads. So the bar
   stays uncovered, and it is lifted above the overlay for the duration so its
   popups — which drop down into the covered area — stay usable too. The lift
   is scoped to the loading state on purpose: the full-screen modals on the
   profile and jobs pages are meant to cover the navbar, and a permanent raise
   would put it through them. */
.nd-page-loading {
  position: fixed;
  top: 60px; /* the fixed navbar's height, at every breakpoint */
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 900;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: var(--nd-scrim);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  opacity: 0;
  transition: opacity 0.2s ease;
  overscroll-behavior: contain;
  touch-action: none;
}

.nd-page-loading.visible {
  opacity: 1;
}

body.nd-page-loading-active .nav-top {
  z-index: 950;
}

/* Small on purpose: this is a wait, not an event. */
.nd-page-loading-pill {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 14px 24px;
  border-radius: 999px;
  background-color: var(--nd-glass-fill);
  backdrop-filter: blur(22px) saturate(180%);
  -webkit-backdrop-filter: blur(22px) saturate(180%);
  box-shadow:
    inset 0 0 0 1px var(--nd-glass-rim),
    inset 0 1px 0 var(--nd-glass-sheen),
    0 12px 30px var(--nd-shadow-ink);
  font-family: var(--nd-font);
  font-size: 16px;
  color: var(--nd-text);
}

.nd-page-loading-spinner {
  width: 20px;
  height: 20px;
  flex: none;
  border-radius: 50%;
  border: 2px solid var(--nd-glass-rim);
  border-top-color: var(--nd-action);
  animation: ndPageLoadingSpin 0.8s linear infinite;
}

@keyframes ndPageLoadingSpin {
  to { transform: rotate(360deg); }
}

/* Reduced motion: the frame still blocks and still says what it is waiting
   for, but the ring holds still rather than spinning. */
@media (prefers-reduced-motion: reduce) {
  .nd-page-loading {
    transition: none;
  }

  .nd-page-loading-spinner {
    animation: none;
    border-top-color: var(--nd-text-muted);
  }
}

/* ---------------- Language switch overlay ---------------- */
/* Shown by JS/i18n.js while a language change is in flight. It lives here
   rather than in a page stylesheet because translation is available on every
   page — the ones with a navbar get i18n.js injected, the rest load it
   directly, and both need this.

   It exists to hold the page still. The swap rewrites text nodes all over the
   document, so a click landing mid-swap can hit a button whose label has
   already changed under the cursor; the overlay takes the pointer and the
   keyboard until the text has settled. It sits above the page transition
   curtain (10000 on the landing page), the highest thing in the app. */
.nd-lang-overlay {
  position: fixed;
  inset: 0;
  z-index: 10050;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: var(--nd-scrim);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  opacity: 0;
  transition: opacity 0.18s ease;
  /* No scroll chaining to the page underneath on touch. */
  overscroll-behavior: contain;
  touch-action: none;
}

.nd-lang-overlay.visible {
  opacity: 1;
}

/* Same frosted material as the rest of the app's floating chrome. */
.nd-lang-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  min-width: 280px;
  max-width: calc(100vw - 48px);
  padding: 36px 44px;
  border-radius: 24px;
  text-align: center;
  background-color: var(--nd-glass-fill);
  backdrop-filter: blur(22px) saturate(180%);
  -webkit-backdrop-filter: blur(22px) saturate(180%);
  box-shadow:
    inset 0 0 0 1px var(--nd-glass-rim),
    inset 0 1px 0 var(--nd-glass-sheen),
    0 20px 50px var(--nd-shadow-ink-strong);
  transform: scale(0.96);
  transition: transform 0.18s ease;
}

.nd-lang-overlay.visible .nd-lang-card {
  transform: scale(1);
}

/* The globe sits still inside a ring that sweeps around it — the ring is the
   only moving part, so the glyph stays readable while it spins. */
.nd-lang-ring {
  position: relative;
  width: 62px;
  height: 62px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--nd-text);
}

.nd-lang-ring::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 2px solid var(--nd-glass-rim);
  border-top-color: var(--nd-action);
  animation: ndLangSpin 0.9s linear infinite;
}

@keyframes ndLangSpin {
  to { transform: rotate(360deg); }
}

.nd-lang-title {
  margin: 0;
  font-family: var(--nd-font);
  font-size: 14px;
  font-weight: 400;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--nd-text-muted);
}

.nd-lang-target {
  margin: 0;
  font-family: "Roboto", sans-serif;
  font-size: 26px;
  font-weight: 500;
  line-height: 1.2;
  color: var(--nd-text);
}

/* The greeting cycles through scripts while the swap runs: it says "the
   platform is changing language" faster than a progress bar could, and its
   fixed height keeps the card from resizing on every change. */
.nd-lang-greeting {
  min-height: 26px;
  font-family: "Roboto", sans-serif;
  font-size: 19px;
  line-height: 26px;
  color: var(--nd-text-lilac);
  animation: ndLangGreet 0.45s ease;
}

@keyframes ndLangGreet {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Reduced motion: the frame still blocks and still says what it's doing, but
   nothing spins and the greeting doesn't cycle (JS stops swapping it). */
@media (prefers-reduced-motion: reduce) {
  .nd-lang-overlay,
  .nd-lang-card,
  .nd-lang-greeting {
    transition: none;
    animation: none;
  }

  .nd-lang-ring::before {
    animation: none;
    border-top-color: var(--nd-glass-rim);
  }
}

/* ---------------- "A new version is ready" ---------------- */
/* The bar JS/update-notice.js raises when the build on the server stops being
   the build in this tab. See that file for when it appears; this is only what
   it looks like when it does.

   Bottom centre, floating, narrow. Not a banner across the top: the top of
   every page here already belongs to the navbar, and pushing the whole page
   down to say something optional is a bigger interruption than the news
   deserves. Bottom centre is where a page puts a thing you may ignore.

   z-index 90 puts it over the menus and dialogs (60) and under the full-screen
   modals (100+) — see the band described in Navbar.css. A reload prompt should
   not be hidden behind a dropdown, and should not sit on top of the contract
   modal somebody is reading. */
.nd-update {
  position: fixed;
  z-index: 90;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: calc(100vw - 32px);
  padding: 10px 10px 10px 18px;
  border-radius: 999px;
  border: 1px solid var(--nd-border);
  background-color: var(--nd-surface-alt);
  box-shadow: 0 12px 40px var(--nd-shadow-ink);
  animation: nd-update-in 0.24s ease-out;
}

/* Up from the edge it is anchored to, so it reads as arriving rather than
   appearing. Skipped for anyone who has asked for less motion — this is an
   interruption they did not ask for, which is precisely the kind that should
   respect that setting. */
@keyframes nd-update-in {
  from {
    opacity: 0;
    transform: translate(-50%, 12px);
  }
  to {
    opacity: 1;
    transform: translate(-50%, 0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .nd-update {
    animation: none;
  }
}

.nd-update-text {
  margin: 0;
  font-size: 14px;
  color: var(--nd-text);
}

.nd-update-btn {
  flex: 0 0 auto;
  padding: 8px 18px;
  border: none;
  border-radius: 999px;
  background-color: var(--nd-action);
  color: var(--nd-on-accent);
  font-family: inherit;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
.nd-update-btn:hover:not(:disabled) {
  box-shadow: 0 0 18px 4px var(--nd-glow-action-strong);
}
.nd-update-btn:disabled {
  opacity: 0.6;
  cursor: default;
}

/* Quiet, and quiet on purpose: "not now" is a real answer here, but it is not
   the one the bar is asking for. */
.nd-update-close {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  border: none;
  border-radius: 50%;
  background: none;
  color: var(--nd-text-muted);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
}
.nd-update-close:hover {
  background-color: var(--nd-hover-wash);
  color: var(--nd-text);
}

.nd-update-btn:focus-visible,
.nd-update-close:focus-visible {
  outline: 2px solid var(--nd-purple);
  outline-offset: 2px;
}

/* On a phone the pill becomes a bar with room for its own words. */
@media (max-width: 480px) {
  .nd-update {
    left: 16px;
    right: 16px;
    bottom: 16px;
    max-width: none;
    transform: none;
    border-radius: 16px;
  }
  @keyframes nd-update-in {
    from { opacity: 0; }
    to { opacity: 1; }
  }
}
