@layer base, vendor, components;

/* Design tokens, and the order of the cascade.

   Every other stylesheet in this directory is one component, wrapped in a
   layer. Propshaft's `stylesheet_link_tag :app` links all of them, sorted by
   filename — so the order they arrive in is alphabetical and means nothing.
   The layer statement below is what actually decides who wins, once, here.
   Adding a component is adding a file; it is never a question about where in
   a big stylesheet it has to go to work.

   This file sorts first because of the underscore, which is the only reason
   the layer statement is read before the layers it orders.

   The tokens themselves stay outside every layer. Unlayered normal
   declarations beat layered ones, which is the right way round: a component
   may not quietly redefine the scale it is drawn on.

   Component CSS uses these and never raw values. Dark mode via light-dark(),
   so there is no class to toggle and no flash.

   The scale is deliberately large. This is a tool a studio has open all day on
   a desk, not a dashboard to be skimmed, and the work it shows is a handful of
   sentences — a task title, a name, a date. When there is that little to say,
   setting it small to fit more in is a false economy: it makes a short list
   look like a long one. HEY's sizing argument, and it is the right one here.

   Three rules the components follow:
     1. Body text is 19px and never smaller than 15px anywhere.
     2. Anything clickable is at least 44px tall. Most are 52px.
     3. Separation is space and tint before it is a line. Lines are the last
        resort, not the default. */

:root {
  color-scheme: light dark;

  /* ---- Type ------------------------------------------------------------
     Two roles. The sans carries everything a person wrote; the mono carries
     everything the system assigned — task references, counts, dates as data.
     That split is the typography doing the same job the product does:
     telling identity apart from content. */

  --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
               "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
               "Liberation Mono", monospace;

  /* Anchored high and allowed to flow (tech arch §8). The steps are far
     enough apart to be told apart at a glance — a scale whose neighbours are
     one pixel apart is not a hierarchy, it is a rounding error. */
  --text-xs:   0.9375rem;                                  /* 15px  labels    */
  --text-sm:   1rem;                                       /* 16px  secondary */
  --text-base: clamp(1.125rem, 0.3vw + 1.05rem, 1.1875rem); /* 19px  body     */
  --text-md:   clamp(1.25rem, 0.5vw + 1.13rem, 1.375rem);  /* 22px  row title */
  --text-lg:   clamp(1.5rem, 1vw + 1.25rem, 1.875rem);     /* 30px  section   */
  --text-xl:   clamp(2rem, 2.2vw + 1.4rem, 2.75rem);       /* 44px  view name */
  --text-2xl:  clamp(2.5rem, 3.4vw + 1.6rem, 3.75rem);     /* 60px  the void  */

  --leading-tight: 1.1;
  --leading-snug:  1.3;
  --leading-body:  1.6;

  /* Large type set at default tracking looks loose; small caps set at default
     tracking looks cramped. Both are corrected here rather than per component. */
  --tracking-tight:  -0.021em;
  --tracking-normal: 0;
  --tracking-label:   0.07em;

  --weight-normal: 400;
  --weight-medium: 500;
  --weight-bold:   600;
  --weight-heavy:  700;

  /* ---- Space -----------------------------------------------------------
     One scale, multiples of --space. Nothing in a component may invent a
     value between two steps; if the step is wrong, the step is wrong. */
  --space:     0.5rem;   /*  8 */
  --space-2:   1rem;     /* 16 */
  --space-3:   1.5rem;   /* 24 */
  --space-4:   2rem;     /* 32 */
  --space-5:   2.5rem;   /* 40 */
  --space-6:   3rem;     /* 48 */
  --space-8:   4rem;     /* 64 */
  --space-10:  5rem;     /* 80 */
  --space-12:  6rem;     /* 96 */

  /* ---- Size ------------------------------------------------------------
     Controls are one of two heights and rows are one height, so a form and a
     list set next to each other share a rhythm instead of nearly sharing one. */
  --control-h:     3.25rem;  /* 52px — inputs, buttons, the capture field */
  --control-h-sm:  2.75rem;  /* 44px — the floor for anything clickable   */
  --row-h:         4rem;     /* 64px — one task                          */

  /* The list is four fixed rails and one elastic title, so the references,
     dates and faces line up down the page as columns while the title takes
     whatever is left. Ragged right-hand edges are what make a calm list look
     like a receipt.

     Every one of these is measured against what it actually holds. --rail-when
     was 10rem when it carried a status word as well as a date; the word moved
     to the mark at the head of the row and the rail did not follow it, which
     cost the title 48px it had no reason to give up. */
  --rail-status: 1.75rem; /* the progress mark */
  --rail: 4.75rem;        /* ACME-142 */
  --rail-when: 7rem;      /* a date: "Yesterday", "12 Mar 2027" */
  --rail-who: 3.5rem;     /* one face, and the supporter behind it */
  /* The pull sets over two lines — NOT above TODAY — so the rail is sized to
     the longer *word* rather than to the phrase. At one line it wanted 107px
     and only ever had 96, so it was overhanging into the faces beside it; set
     to fit "TODAY" it needs 69px and the 20px it gives back go to the title.
     Anything in [69px, 107px) breaks in the same place; this sits inside that
     window with room for a font that measures wider than the stack's first
     choice. */
  --rail-pull: 4.75rem;   /* "Not today", over two lines */

  /* The members row's two, measured the same way: what they actually hold. */
  --rail-role: 8rem;      /* the role picker: "Member" and a chevron */
  --rail-remove: 6.5rem;  /* "Remove" */

  /* Where the title column starts. An empty state, a first run and the line
     that says a list stopped all hang off it, so that a list ending looks
     like the list ending rather than like a hole beside it. */
  --list-indent: calc(var(--rail-status) + var(--rail) + var(--space-2) * 2);

  /* The page's side margin, and the height of the bar at the top of it.
     Both are declared rather than left to content, because the two menus that
     hang off the masthead are popovers — and a popover lives in the top layer,
     where it can be positioned against the viewport and nothing else. It
     cannot measure the bar it belongs to, so the bar is a known quantity.

     On a phone the masthead wraps to two rows: you and the team on the first,
     capture on the second. Two controls, three gaps. */
  --gutter: var(--space-4);
  --masthead-h: calc(var(--control-h) + var(--space-2) * 2);

  /* Installed, the app has no browser chrome to sit under, and the layout says
     viewport-fit=cover — which puts the page *under* the status bar and the
     home indicator rather than beside them. That is the right default for a
     background that should run to the edges, and the wrong one for anything
     you have to read or press, so everything that lives at an edge measures
     from these. Zero in a browser, and on any phone without a notch. */
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-right: env(safe-area-inset-right, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);

  --radius-sm:  0.5rem;
  --radius:     0.875rem;
  --radius-lg:  1.25rem;
  --radius-pill: 999px;

  /* ---- Colour ----------------------------------------------------------
     By role, never by hue. Paper is warm and slightly off-white; the dark
     side is a blue-grey rather than black, because true black under a large
     type scale reads as a hole rather than a page. */

  --canvas:        light-dark(#fcfbf8, #131417);
  --surface:       light-dark(#ffffff, #1b1d21);
  --surface-sunk:  light-dark(#f4f2ec, #16181b);
  --surface-hover: light-dark(#f6f4ee, #22252a);

  --ink:        light-dark(#191a1d, #f1f2f4);
  --ink-muted:  light-dark(#5c5f66, #a4a9b2);
  /* Faint is the quietest ink there is, and it still has to be readable: it
     carries the 14px labels, the task references and the counts. The previous
     pair measured 3.2:1 and 4.2:1 against the canvas, both under AA for text
     at that size. These clear 4.5:1 on the canvas and on the sunk surface. */
  --ink-faint:  light-dark(#6b6e76, #8f949d);

  --line:        light-dark(#eae6dd, #262a2f);
  --line-strong: light-dark(#d9d4c8, #363b42);

  /* Ink blue: a written blue rather than a screen blue, dark enough to sit
     under body text without vibrating against the warm paper. */
  --accent:      light-dark(#2b4ed6, #8aa5ff);
  --accent-ink:  light-dark(#ffffff, #0f1116);
  --accent-sunk: light-dark(#ecefff, #1c2440);

  /* Hover for the one thing painted in the accent. A colour rather than a
     filter: brightness() lightens on both sides of the theme, which on the
     dark side moves the accent *towards* the page rather than away from it —
     the pointer's feedback is strongest where it is needed least. */
  --accent-hover: light-dark(#2440b8, #a3b8ff);

  /* Three signals, and no fourth. Ochre is Waiting On, which is the state the
     product exists to make visible (D-03), so it is the warmest thing on the
     page and the only colour allowed to interrupt a list. */
  --go:        light-dark(#1c6b46, #6ec994);
  --go-sunk:   light-dark(#e6f2ea, #16251d);
  --warn:      light-dark(#96590a, #e3a651);
  --warn-sunk: light-dark(#fbf0dd, #2a2013);
  --danger:    light-dark(#a52a20, #f0837c);

  --focus-ring: 2.5px solid var(--accent);
  --focus-offset: 3px;

  /* The same ring, drawn inside the element instead of around it. For controls
     that sit flush in a boundary — a toast's dismiss, a picker in a row — where
     an outset ring would be clipped by the thing it is flush against. It is a
     token so that "inside" is one decision made once, rather than each
     component inventing its own negative number. */
  --focus-offset-inset: -3px;

  /* One shadow, used on the things that lift off the page and nothing else. */
  --shadow-card: 0 1px 2px light-dark(rgb(60 50 30 / 5%), rgb(0 0 0 / 40%)),
                 0 8px 24px light-dark(rgb(60 50 30 / 5%), rgb(0 0 0 / 28%));

  /* Two widths, and the difference between them is the point. --measure is
     how wide a paragraph may be before the eye loses the start of the next
     line; --column is how wide the page is. Prose is pinned to the first
     wherever it appears — notes, the timeline, every standfirst — so the
     second is free to be as wide as the work is, and the work is a list of
     one-line titles that were being cut off at 36 characters with 400px of
     window either side of them. */
  --measure: 42rem;
  --column: 68rem;

  /* And there are two, which is the rule. A panel, a card, a list and a page
     header take --column; anything read as prose or typed into takes
     --measure, wherever it happens to sit. Forms used to hold a third width of
     their own at 32rem, so the Members page could show four different right
     edges at once — the list at the column, the standfirst at the measure, and
     two cards at neither. Nesting one inside the other reads as a panel
     containing a form. Setting them side by side read as a mistake. */
}

@media (max-width: 48rem) {
  :root {
    --gutter: var(--space-3);
    --masthead-h: calc(var(--control-h) * 2 + var(--space) * 3);
  }
}

/* Touch has no hover to reveal the pull with, so it is always on screen and
   drawn as a pill — a border and two more paddings than the pointer control,
   which is exactly the width the narrower rail gives up. So touch keeps the
   old one. Measured: TODAY sets 52px, and the pill leaves it 2px inside a
   5.5rem rail and 10px inside 6rem; the rail is not where that margin should
   be spent. A tablet in landscape is the case this is for — wide enough for
   the full row of rails, touch enough for the pill. Phones lay the row out in
   three lines and never read this. */
@media (hover: none) {
  :root { --rail-pull: 6rem; }
}

/* ---- Motion ------------------------------------------------------------
   Five layers, and every animated thing in the product belongs to exactly one.
   The layer decides the duration, the easing and what happens under reduced
   motion, so no component is ever choosing a number for itself.

     Touch    what your hands are doing — hover, focus, press. Too fast to
              register as animation; you notice only its absence.
     State    the data changed. Long enough to be seen on a page you were not
              looking at when it happened.
     Arrival  something entered or left. Enters softly, leaves briskly.
     Travel   you moved, and the same object is in both places. The browser
              does this one, through view-transition-name.
     Ambient  the page saying where you are in it. Scroll-linked, no duration.
              Exactly one thing qualifies and it stays that way.

   Two easings and no third. Arriving decelerates into place; leaving
   accelerates away, because nobody needs to watch something they have
   finished with. --ease-land is the one exception and it is spent on the
   focus ring, where a hair of overshoot is what makes the ring read as
   landing on a control rather than appearing around it. */

:root {
  --ease:      cubic-bezier(0.2, 0, 0.1, 1);
  --ease-exit: cubic-bezier(0.4, 0, 1, 1);
  --ease-land: cubic-bezier(0.2, 0.9, 0.3, 1.1);

  /* The press. Down fast and up slow: at the same speed both ways it reads as
     a wobble, and the ratio between them is the whole of what makes it read as
     weight instead. Scale rather than translate, because a control that moves
     down moves its label out from under the pointer that is pressing it.

     Deliberately not in the reduced-motion branch below. This is not
     decoration — it is the control saying it heard you — and 3.5% over 110ms
     is nobody's vestibular trigger. Somebody who asked for less movement has
     not asked to be told less. */
  --press:      scale(0.965);
  --press-down: 45ms;
  --transition-press: 110ms var(--ease);

  /* What it is to be a control here: it takes the press, and the ring lands on
     it. Named once so a component adds one word to its own transition list
     rather than restating three declarations — and so that the day the press
     changes, it changes everywhere.

     It has to be added by hand because `transition` is a shorthand: a
     component that sets its own replaces this wholesale rather than adding to
     it. See base.css, where the rest state lives. */
  --responds: transform var(--transition-press),
              outline-color var(--transition),
              outline-offset var(--transition-focus);
}

@media (prefers-reduced-motion: no-preference) {
  :root {
    --transition:       130ms var(--ease);  /* touch                       */
    --transition-focus: 130ms var(--ease-land);
    --transition-enter: 220ms var(--ease);  /* arrival, and state          */
    --transition-exit:  150ms var(--ease-exit);
    --transition-slow:  340ms var(--ease);  /* one per screen, at most     */

    /* Travel is a duration on its own rather than a transition, because the
       browser applies it to an animation it wrote itself. Longer than an
       arrival: a title crossing the page and growing from 22px to 44px is the
       furthest anything moves in this product, and at 220ms it reads as a jump
       with a blur on it. Only ever read inside a no-preference query — Turbo
       declines to start a view transition under reduce before this is asked
       for. */
    --travel: 280ms;
  }
}

/* Not zero across the board. Under reduce a thing still *arrives* — it simply
   arrives where it is, rather than travelling there — so what goes is the
   distance and not the fact that something changed. A row that vanishes
   between two frames is harder to follow than one that fades over 120ms,
   which is the opposite of what the setting is asking for.

   Note that this makes flash_controller's transitionend the normal path
   rather than the fallback it used to be at 0s. Its timeout still backs it up
   and the comment there says why. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --transition:       0s;
    --transition-focus: 0s;
    --transition-enter: 120ms linear;
    --transition-exit:  100ms linear;
    --transition-slow:  120ms linear;
  }
}
