@tailwind base;
@tailwind components;
@tailwind utilities;

@font-face {
  font-family: 'Manrope Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 200 800;
  src: url('@fontsource-variable/manrope/files/manrope-latin-wght-normal.woff2') format('woff2-variations');
}

/*
 * Surfaces and neutrals.
 *
 * Accent variables are NOT defined here — they are written onto the root
 * element at runtime from the palette in @pig/core, so there is exactly one
 * definition of each colour and the CSS cannot drift from the TypeScript.
 */
:root {
  /*
   * The canvas is a shade off white while cards stay pure white. When both were
   * 0 0% 100% a card's shadow was the only thing separating it from the page,
   * so any surface that opts out of shadow — the Piggy workspace does — merged
   * into one undifferentiated white field. Dark theme has always had this
   * separation (4% canvas against 7% surface); light now reads as three planes
   * too. Surface stays lighter than the canvas, so the card still lifts.
   */
  --bg: 240 5% 98%;
  --surface: 0 0% 100%;
  --surface-2: 240 5% 97%;
  --border: 240 6% 90%;
  --fg: 240 10% 4%;
  --muted: 240 4% 46%;

  /*
   * Status hues in light theme are darkened to clear 4.5:1 as text on a white
   * card AND on their own /10 fill — the chip is the case that failed, because
   * a tinted fill lifts the background under the very text it is tinting.
   * "Needs you" on a pending approval measured 2.84:1 on its own chip, which
   * made the one badge in the product that means "act now" the least legible
   * thing on the page. Dark theme measured clean and is deliberately untouched.
   *
   * The first pass tuned these against WHITE and left `warning` at 4.35:1 and
   * `positive` at 4.37:1 once the /10 fill was composited — and a badge never
   * renders on white, it renders on its own tint, which is exactly where the
   * ratio is lowest. Worse, a tinted row inside a card sits on `--surface-2`,
   * not on `--surface`, which takes another tenth off. Both are now measured
   * against the darkest ground either actually lands on (`--surface-2` under
   * the /10 fill), so the stated goal — every tone clears 4.5:1 — is true on
   * the surfaces that ship rather than on a background the product never uses.
   */
  --positive: 160 84% 24%; /* 6.03:1 on surface, 4.90:1 on a /10 chip over surface-2 */
  --warning: 32 95% 31%; /* 5.76:1 on surface, 4.70:1 on a /10 chip over surface-2 */
  --danger: 0 72% 45%; /* 5.83:1 on surface, 4.65:1 on its own tint — already clear */
  --info: 201 90% 32%; /* 6.06:1 on surface, 4.92:1 on its own tint — already clear */
  --shadow: 240 10% 4%;

  /*
   * Motion. Three durations and two easings, by role, so a component reaches
   * for a name instead of inventing a number: every hand-written duration in
   * the tree is one more thing that has to be re-decided in review.
   *
   * Reachable from Tailwind as duration-1/2/3 and ease-enter/ease-exit — see
   * the transitionDuration and transitionTimingFunction blocks in
   * tailwind.config.js, which alias these rather than restating the values.
   */
  --dur-1: 120ms; /* colour, opacity, hover, focus, disclosure chevron */
  --dur-2: 180ms; /* popovers, dropdowns, tooltips, badges appearing */
  --dur-3: 240ms; /* sheets, drawers, dialogs */
  --ease-out: cubic-bezier(0.2, 0, 0, 1); /* enter */
  --ease-in: cubic-bezier(0.4, 0, 1, 1); /* exit */

  /* Safe-area insets, so layout can reference them even at zero. */
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);
  --safe-right: env(safe-area-inset-right, 0px);

  /*
   * The application header's height, including the notch it sits under. Both
   * side panes are sticky beneath it and subtract this from the viewport, so
   * it has to be one number rather than a magic constant in three files.
   */
  --app-header-h: calc(3.5rem + var(--safe-top));

  /*
   * The sidebar primitive's palette.
   *
   * Aliases, not values. shadcn's sidebar block wants its own `--sidebar-*`
   * scale; defining literal colours here would be a second palette to keep in
   * step with the first, and it would not follow the user's accent — which is
   * written onto this same element at runtime by lib/theme.tsx. Because these
   * are HSL channel triples pointing at other HSL channel triples, the dark
   * theme and every accent flow through for free and there is nothing to
   * duplicate under [data-theme='dark'].
   */
  --sidebar-background: var(--surface);
  --sidebar-foreground: var(--fg);
  --sidebar-primary: var(--accent);
  --sidebar-primary-foreground: var(--accent-on);
  --sidebar-accent: var(--accent-subtle);
  --sidebar-accent-foreground: var(--accent-fg);
  --sidebar-border: var(--border);
  --sidebar-ring: var(--accent);
}

@media (min-width: 1024px) {
  :root {
    --app-header-h: calc(4rem + var(--safe-top));
  }
}

:root[data-theme='dark'] {
  --bg: 240 10% 4%;
  --surface: 240 8% 7%;
  --surface-2: 240 6% 11%;
  --border: 240 5% 18%;
  --fg: 0 0% 98%;
  --muted: 240 5% 60%;

  --positive: 158 64% 52%;
  --warning: 38 92% 60%;
  --danger: 0 84% 65%;
  --info: 199 89% 60%;
  --shadow: 0 0% 0%;
}

@layer base {
  * {
    border-color: hsl(var(--border));
  }

  html {
    /* Prevents iOS from silently enlarging text in landscape. */
    -webkit-text-size-adjust: 100%;
    /* Keeps the background painted behind the rubber-band overscroll area. */
    background-color: hsl(var(--bg));
  }

  body {
    @apply bg-bg text-fg antialiased;
    font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
    /* Stops the whole page bouncing on iOS while inner panes still scroll. */
    overscroll-behavior-y: none;
  }

  ::selection {
    background: hsl(var(--accent-subtle));
    color: hsl(var(--accent-fg));
  }

  /*
   * Mobile Safari zooms the viewport when a focused input has a font size
   * below 16px, and never zooms back out. This is the fix — not
   * user-scalable=no, which breaks pinch-zoom for everyone.
   */
  input,
  select,
  textarea {
    font-size: max(16px, 1rem);
  }

  /*
   * A visible, consistent focus ring — keyboard users need it, and the default
   * varies wildly between browsers.
   *
   * The ring is `brand`, not `accent`. Tailwind's `accent` is aliased to
   * --accent-subtle (shadcn's hover surface), which measured 1.10:1 in light
   * and 1.34:1 in dark against the canvas: a focus ring nobody could see, on
   * every control in the product. --accent proper measures 15.9:1 / 18.6:1.
   */
  :focus-visible {
    @apply outline-none ring-2 ring-brand ring-offset-2 ring-offset-bg;
  }

  /* Respect a reduced-motion preference rather than animating regardless. */
  @media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }

    /*
     * Progress is the deliberate exemption. A spinner is a steady rotation, not
     * the parallax or zoom the preference exists to suppress, and the blanket
     * rule froze all 16 spinner sites one frame in — including the seconds
     * between pressing Apply and the write landing, where a stopped spinner
     * reads as a hung request. It keeps spinning, slower.
     *
     * Higher specificity than the `*` rule above, so it wins between two
     * !important declarations regardless of source order.
     */
    .animate-spin {
      animation-duration: 1.2s !important;
      animation-iteration-count: infinite !important;
    }
  }
}

@layer components {
  /* Horizontal scrollers (pipeline boards, wide tables) get momentum
     scrolling and snap on touch, without hijacking the page. */
  .scroll-x {
    @apply overflow-x-auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
    overscroll-behavior-x: contain;
  }

  .scroll-x::-webkit-scrollbar {
    height: 6px;
  }
  .scroll-x::-webkit-scrollbar-thumb {
    background-color: hsl(var(--border));
    border-radius: 3px;
  }

  /* The expanded sidebar owns its vertical scroll rather than the page. Keep
     that scrollbar inside the sidebar palette: the browser's native light
     track otherwise becomes a bright gutter in dark mode. */
  [data-sidebar='content'] {
    scrollbar-color: hsl(var(--sidebar-border)) transparent;
    scrollbar-width: thin;
  }

  [data-sidebar='content']::-webkit-scrollbar {
    width: 6px;
  }

  [data-sidebar='content']::-webkit-scrollbar-track,
  [data-sidebar='content']::-webkit-scrollbar-corner {
    background: transparent;
  }

  [data-sidebar='content']::-webkit-scrollbar-thumb {
    background-color: hsl(var(--sidebar-border));
    border-radius: 9999px;
  }

  [data-sidebar='content']::-webkit-scrollbar-thumb:hover {
    background-color: hsl(var(--muted) / 0.55);
  }

  /* Touch targets. 44px is the documented iOS minimum and the difference
     between a usable and a frustrating phone experience. */
  .tap {
    @apply min-h-[44px] min-w-[44px];
  }

  .card {
    @apply rounded-2xl border border-border/90 bg-surface;
    box-shadow: 0 1px 2px hsl(var(--shadow) / 0.035), 0 12px 36px hsl(var(--shadow) / 0.025);
  }

  .app-canvas {
    background-color: hsl(var(--bg));
    background-image:
      radial-gradient(circle at 78% -12%, hsl(var(--accent-subtle) / 0.7), transparent 34rem),
      linear-gradient(to bottom, hsl(var(--surface) / 0.25), transparent 28rem);
    background-attachment: fixed;
  }

  /*
   * The auth surface is intentionally monochrome even if a signed-in user has
   * selected another accent. It is public brand chrome, and forcing the local
   * dark token set keeps controls legible without changing the stored theme.
   */
  .auth-shell {
    --bg: 240 10% 3%;
    --surface: 240 7% 6%;
    --surface-2: 240 5% 11%;
    --border: 240 4% 22%;
    --fg: 0 0% 98%;
    --muted: 240 5% 67%;
    --accent: 0 0% 98%;
    --accent-fg: 0 0% 98%;
    --accent-on: 240 6% 10%;
    --accent-subtle: 240 4% 16%;
    color-scheme: dark;
  }

  .auth-field-motion {
    animation: auth-field-drift 26s ease-in-out infinite alternate;
    will-change: transform;
  }

  .auth-visual {
    opacity: 0.52;
  }

  .auth-visual::after {
    position: absolute;
    inset: 0;
    content: '';
    background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.92' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.72'/%3E%3C/svg%3E");
    background-size: 180px 180px;
    mix-blend-mode: soft-light;
    opacity: 0.14;
  }

  .auth-visual-vignette {
    background:
      linear-gradient(to bottom, hsl(var(--bg) / 0.18), hsl(var(--bg) / 0.72) 34%, hsl(var(--bg)) 100%),
      linear-gradient(to right, hsl(var(--bg) / 0.18), hsl(var(--bg) / 0.42));
  }

  @media (min-width: 1024px) {
    .auth-visual {
      opacity: 1;
    }

    .auth-visual-vignette {
      background:
        linear-gradient(to right, transparent 48%, hsl(var(--bg) / 0.28) 78%, hsl(var(--bg) / 0.72)),
        linear-gradient(to bottom, hsl(var(--bg) / 0.06), transparent 18%, hsl(var(--bg) / 0.28));
    }
  }

  @keyframes auth-field-drift {
    0% {
      transform: translate3d(-1.4%, -0.8%, 0) scale(1.045);
    }
    50% {
      transform: translate3d(0.8%, 0.6%, 0) scale(1.075);
    }
    100% {
      transform: translate3d(-0.3%, 1.1%, 0) scale(1.055);
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .auth-field-motion {
      animation: none;
      transform: scale(1.045);
      will-change: auto;
    }
  }

  /* Tabular figures keep money and percentages from jittering as they
     update, which matters on a dashboard that refreshes. */
  .nums {
    font-variant-numeric: tabular-nums;
    font-feature-settings: 'tnum';
  }

  /*
   * The page KPI figure fits its tile instead of breaking across two lines.
   *
   * `break-words` on `Stat` stopped the figure being CLIPPED — measured
   * `$658,194.3`, a digit short — but it did it by breaking the number, so
   * `GROSS MARGIN / $658,194. / 37` passed every `scrollWidth === clientWidth`
   * check while reading as two numbers. On a brokerage instrument that is the
   * worse failure of the two: a clipped figure looks broken, a split one looks
   * wrong. Measured at 30px, `$658,194.37` is 185px wide and the tile it has to
   * live in is 149px at 1440 with the Piggy dock open and 104px on a 320px
   * phone, so no single size fits every column count the grids use.
   *
   * A container query is the honest instrument: the tile asks how much room it
   * was given and picks a step, so a four-up row at 1440 reads at 30px, the
   * same row with the dock open reads at 22px, and a phone reads at 20px or 18.
   * Every tile in a grid row is the same width, so a row steps together and
   * never mixes two sizes. The steps below are the measured fits for the
   * longest figure in the product with a few pixels of headroom each; the
   * `break-words` fallback stays as the last resort so nothing can ever clip.
   *
   * The selectors are `.stat-tile .stat-figure-lg` rather than the figure's
   * class alone, and that is load-bearing rather than decorative: `@layer
   * components` is emitted BEFORE `@layer utilities`, so a single-class rule
   * here loses to `text-2xl` on source order and the ladder silently did
   * nothing. Two classes is 0-2-0 against a utility's 0-1-0, which wins
   * wherever the layers sit. The figure is always inside the tile that declares
   * the container, so the descendant selector costs nothing.
   */
  .stat-tile {
    container-type: inline-size;
  }

  /* 24px. Measured need for the longest figure at this step: 148px. */
  @container (max-width: 192px) {
    .stat-tile .stat-figure-lg {
      font-size: 1.5rem;
    }
  }

  /* 22px — needs 136px. This is the 1440-with-the-dock-open case, at 149px. */
  @container (max-width: 156px) {
    .stat-tile .stat-figure-lg {
      font-size: 1.375rem;
    }
  }

  /* 20px — needs 124px. A 393px phone gives the figure 140px. */
  @container (max-width: 144px) {
    .stat-tile .stat-figure-lg {
      font-size: 1.25rem;
    }
  }

  /* 18px — needs 111px. A 360px phone gives 124px. */
  @container (max-width: 130px) {
    .stat-tile .stat-figure-lg {
      font-size: 1.125rem;
    }
  }

  /* 16px — needs 99px, and a 320px phone gives 104px. The floor: below this a
     figure is no longer a headline, and `break-words` takes over. */
  @container (max-width: 118px) {
    .stat-tile .stat-figure-lg {
      font-size: 1rem;
    }
  }

  /*
   * The file picker's own button.
   *
   * `Input[type=file]`'s shell was styled and `::file-selector-button` was not,
   * so the control a person actually presses on the data-onboarding page was
   * the platform's own bevelled chrome — wrong font, wrong radius, and about
   * 103x25px inside a 44px field. It is the one control in the product that had
   * never been dressed.
   */
  input[type='file']::file-selector-button {
    margin-right: 0.75rem;
    height: 2rem;
    padding: 0 0.75rem;
    border: 1px solid hsl(var(--border));
    border-radius: 0.5rem;
    background: hsl(var(--surface-2));
    color: hsl(var(--fg));
    font-family: inherit;
    font-size: 0.8125rem;
    font-weight: 500;
    cursor: pointer;
    transition: background-color var(--dur-1) var(--ease-out);
  }

  input[type='file']::file-selector-button:hover {
    background: hsl(var(--border) / 0.6);
  }
}
