/* Soroudi base — the layout language every dashboard shares.
 *
 * THIS FILE IS BYTE-IDENTICAL IN soroudi-hub, soroudi-trips, soroudi-purchases,
 * soroudi-medical AND soroudi-finance. The hub is upstream: change it here and
 * copy it to all five, exactly as public/nav.js already works. A per-app tweak
 * belongs in that app's own styles.css, which loads after this and wins.
 *
 * WHERE IT COMES FROM. All of this was worked out in soroudi-trips over a long
 * stretch of tuning, and this file is that work extracted so the other four stop
 * being four different products. Trips still owns the PALETTE (the six themes in
 * each styles.css) and it owns the design decisions here; what it does not own
 * any more is a private copy of them.
 *
 * WHAT IT DELIBERATELY DOES NOT CONTAIN: anything that knows what an app shows.
 * A trip card, a transaction row and a medication list are different objects and
 * should look different. This file is the grid they sit on -- type scale, the
 * four display preferences, focus, motion, and the settings controls that change
 * them -- not the objects themselves.
 *
 * HOW THE PREFERENCES TRAVEL BETWEEN APPS, which is the part that makes five
 * dashboards feel like one: they are read from `soroudi_display_v1` in
 * localStorage, and since the dashboards were moved under one origin
 * (dash.rajuabju.com/<app>/) that storage is genuinely shared. Setting the text
 * size in Trips sets it in Finance, with no server, no syncing and no API. The
 * theme is the exception and still goes through the SHARED D1 database, because
 * it predates the move and has to survive a new device.
 */

/* ---------------------------------------------------------------------------
   The unit everything is measured in
   --------------------------------------------------------------------------- */

/* --px is one CSS pixel expressed in rem: 1/16rem, exactly 1px while the root
   font size is the 16px browsers ship with. Every size below multiplies by it
   instead of writing px directly.
 *
 * The point is what happens when the root ISN'T 16px. A px is a px whatever the
 * reader has asked for, so a stylesheet written in px ignores the browser's
 * font-size setting and Android's text-scaling slider -- the two controls a
 * person with poor eyesight actually reaches for. In rem it follows them.
 *
 * --fs is separate and does its own job: it is the app's OWN text-size
 * preference. The two multiply, so a reader who has set both gets both. */
:root {
  --px: 0.0625rem;
  --fs: 1.22;
  --lh: 1.5;
  --pad: 1;
  --main-w: 1600px;
  --cards-w: 1000px;
  --detail-w: 1200px;
}

/* A size in the app's scale. Use it for every font-size:
     font-size: var(--t-14);   not   font-size: 14px;
   The named steps exist so a stylesheet does not fill up with calc() and so the
   set of sizes in use stays small enough to be a scale rather than a habit. */
:root {
  --t-11:   calc(11   * var(--px) * var(--fs));
  --t-12:   calc(12   * var(--px) * var(--fs));
  --t-12-5: calc(12.5 * var(--px) * var(--fs));
  --t-13:   calc(13   * var(--px) * var(--fs));
  --t-13-5: calc(13.5 * var(--px) * var(--fs));
  --t-14:   calc(14   * var(--px) * var(--fs));
  --t-14-5: calc(14.5 * var(--px) * var(--fs));
  --t-15:   calc(15   * var(--px) * var(--fs));
  --t-16:   calc(16   * var(--px) * var(--fs));
  --t-17:   calc(17   * var(--px) * var(--fs));
  --t-20:   calc(20   * var(--px) * var(--fs));
  --t-25:   calc(25   * var(--px) * var(--fs));
}

/* ---------------------------------------------------------------------------
   The four display preferences
   ---------------------------------------------------------------------------
   Set as data attributes on <html> by the pre-paint script in each index.html,
   so they apply before the first frame rather than causing a visible reflow. */

:root[data-text="small"]  { --fs: 1; }
:root[data-text="medium"] { --fs: 1.10; }
:root[data-text="large"]  { --fs: 1.22; }
:root[data-text="huge"]   { --fs: 1.38; }

/* Compact and Default both keep one primary column; Compact is just narrower.
   Full is where content is allowed to tile, and only there. */
:root[data-width="compact"] { --main-w: 1160px; --cards-w: 600px;  --detail-w: 720px; }
:root[data-width="default"] { --main-w: 1600px; --cards-w: 1000px; --detail-w: 1200px; }
:root[data-width="full"]    { --main-w: 100%;   --cards-w: 100%;   --detail-w: 96vw; }

:root[data-spacing="tight"]   { --lh: 1.35; }
:root[data-spacing="normal"]  { --lh: 1.5; }
:root[data-spacing="relaxed"] { --lh: 1.75; }

/* Density multiplies padding, not type. An app applies it to its own rows:
     padding: calc(12px * var(--pad)) calc(14px * var(--pad));
   Shrinking the type instead would be a worse trade -- the reader asked for
   less whitespace, not for smaller words. */
:root[data-density="comfortable"] { --pad: 1; }
:root[data-density="cosy"]        { --pad: .78; }
:root[data-density="compact"]     { --pad: .58; }

body { line-height: var(--lh); }

/* The page's own column. Every app's main content sits in one of these so that
   "Compact" and "Full" mean the same thing in all five. */
.wrap { width: 100%; max-width: var(--main-w); margin-inline: auto; }

/* ---------------------------------------------------------------------------
   Settings controls
   ---------------------------------------------------------------------------
   The segmented control the display preferences are chosen with. Identical in
   every app, because a settings screen that differs per dashboard is the first
   place "one product" stops being true. */

.seg { display: flex; gap: 6px; flex-wrap: wrap; margin: 2px 0 16px; }
/* Two rows belonging to one control sit closer than two separate controls do. */
.seg.seg-row-tight { margin-bottom: 6px; }
.seg button {
  border: 1px solid var(--border); background: var(--surface-2); color: var(--text);
  border-radius: 9px; padding: 8px 14px; cursor: pointer;
  font: inherit; font-size: var(--t-13-5); font-weight: 600;
}
.seg button.on { background: var(--accent); border-color: transparent; color: var(--accent-contrast); }
.seg-label {
  font-size: var(--t-12-5); font-weight: 700; color: var(--muted);
  text-transform: uppercase; letter-spacing: .04em;
}
/* A live sample of the setting being chosen, so the choice is visible before it
   is made rather than after. */
.seg-preview {
  border: 1px dashed var(--border); border-radius: 10px; margin-bottom: 4px;
  /* Density-aware, so the preview box reacts to the density setting the same way
     the rows it is previewing do. A fixed padding here would make the preview a
     poor sample of the thing it is meant to preview. */
  line-height: var(--lh); padding: calc(12px * var(--pad)) calc(14px * var(--pad));
}
.seg-preview .p1 { font-weight: 650; font-size: var(--t-14-5); }
.seg-preview .p2 { color: var(--muted); font-size: var(--t-13); margin-top: calc(3px * var(--pad)); }

/* ---------------------------------------------------------------------------
   The app header
   ---------------------------------------------------------------------------
   Measured across the five before this existed: heights of 56, 62, 65, 70 and
   80px, three different class names, brand marks between 20 and 34px, wordmarks
   between 18.3 and 30.5px, and inner columns of 1180px, 1600px and none. Each
   was reasonable on its own and the set was not, which is exactly what you
   notice switching apps inside one installed PWA.

   The geometry is here; the markup stays each app's own. An app opts in by
   giving its header bar `.top` and wrapping its contents in `.top-in`.

   THE SAFE-AREA INSET GOES ON THE BAR, NOT THE CONTENT. On an installed iPhone
   the bar runs under the notch, so the padding that clears it has to sit on the
   element painting the background -- put it on the inner column and the
   background stops short of the top of the screen. Finance had it on the bar's
   BOTTOM padding, which pushed its own content down by the height of a notch it
   was not clearing. */
.top {
  position: sticky; top: 0; z-index: 40;
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  backdrop-filter: saturate(1.4) blur(12px);
  -webkit-backdrop-filter: saturate(1.4) blur(12px);
  border-bottom: 1px solid var(--border);
  padding-top: env(safe-area-inset-top);
}
.top-in {
  max-width: var(--main-w); margin: 0 auto;
  padding: calc(12px * var(--pad)) 16px;
  display: flex; align-items: center; gap: 12px;
}
/* The wordmark is a name, not a control: nothing in the brand should invite a
   tap, because the only header action that navigates is Refresh. */
.brand { display: flex; align-items: center; gap: 9px; min-width: 0; }
/* Scaled with the text, not pinned: trips did this and the others did not, so
   raising the text size grew the wordmark and left the mark beside it stranded
   at its original size. height:auto keeps whatever aspect the file has. */
.brand-mark { width: calc(26 * var(--px) * var(--fs)); height: auto; display: block; flex: 0 0 auto; }
.brand-name {
  font-size: var(--t-17); font-weight: 650; letter-spacing: -.01em;
  /* One line, and the name is what gives way on a narrow phone -- the actions
     beside it are the things a person came to press. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.top-actions { margin-left: auto; display: flex; align-items: center; gap: 8px; }

/* A phone gives back the horizontal padding first, because the wordmark and the
   actions are competing for a 390px line and the actions must not wrap. Two of
   the four apps had this and two did not, which left the bar at three different
   heights on a phone while looking identical on a desktop. */
@media (max-width: 560px) {
  .top-in { padding: calc(10px * var(--pad)) 12px; }

  /* MARK ONLY on a phone. The wordmark is redundant there -- you opened the
     app, and the family nav along the bottom already says which one you are in
     -- and it was not fitting: three of the four truncated their own name to
     "Soroudi Purcha…". Dropping it hands the width to the controls, so nothing
     has to truncate or crowd the mark. Trips worked this out first and had it
     alone; it belongs to all four. */
  .brand-name { display: none; }
  .brand { flex: 0 0 auto; gap: 0; }
  .brand-mark { width: calc(28 * var(--px) * var(--fs)); }
}

/* ONE HEIGHT FOR HEADER CONTROLS, which is what actually made the bars differ.
   The brand row is identical in all four dashboards; the bar around it was 62,
   70, 80 and 60px because the tallest button in each set was 37, 45, 45 and
   35px. A header's height is decided by its biggest control, so that is where
   the consistency has to be enforced -- matching the paddings would only hold
   until one app's button gained a border.

   Scaled with the text like everything else, and comfortably above the 44px
   touch target once the reader has asked for larger type. Vertical padding is
   zeroed because the height is now explicit; the flex centring each app already
   uses does the rest. */
.top-in .btn,
.top-in .icon-btn,
.top-in .mini-btn,
.top-actions > button {
  height: calc(36 * var(--px) * var(--fs));
  min-height: 0;
  padding-top: 0; padding-bottom: 0;
}

/* ---------------------------------------------------------------------------
   A bar that scrolls rather than wraps
   ---------------------------------------------------------------------------
   A row of filters or tabs that wraps to two lines on a phone pushes the content
   down and changes height as it is used. Scrolling keeps the page still. The
   scrollbar is hidden because the row is finger-scrolled, not pointer-scrolled. */
.scroll-x {
  display: flex; gap: 6px; overflow-x: auto; flex-wrap: nowrap;
  -webkit-overflow-scrolling: touch; scrollbar-width: none;
}
.scroll-x::-webkit-scrollbar { display: none; }
.scroll-x > * { flex: 0 0 auto; }

/* Wide content -- a table, a chart -- scrolls inside its own box. The page body
   must never scroll sideways. */
.scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* ---------------------------------------------------------------------------
   Modal sheets
   ---------------------------------------------------------------------------
   Geometry only. What a sheet contains, and how it is painted, stays with the
   app; where it sits and how tall it is are the same question in all of them.
   Trips has no <dialog> at all, so this reaches purchases, medical and finance. */
dialog.sheet {
  width: min(560px, calc(100vw - 24px));
  max-height: min(88vh, 900px);

  /* TOP-ANCHORED, NOT CENTRED. A dialog centred with `margin: auto` re-centres
     itself every time its content changes height -- a photo finishing loading,
     search results arriving, a field appearing as another is filled in -- so the
     whole panel slides upward and downward under the pointer while it is being
     used. Pinning the top edge means only the bottom edge moves, which nobody
     notices and nothing has to chase. */
  margin: 6vh auto auto;
}

@media (max-width: 560px) {
  dialog.sheet {
    /* A bottom sheet on a phone: full width, square top corners, sitting on the
       bottom edge where a thumb is. */
    width: 100vw; max-width: 100vw;
    border-radius: var(--radius) var(--radius) 0 0;

    /* dvh, NOT vh. On a phone browser vh is the LARGE viewport -- the height
       the page would have with the URL bar hidden -- so a sheet sized in vh and
       anchored to the bottom hangs off the screen for as long as that bar is
       showing, and its footer buttons are the part that goes. dvh is the
       viewport as it actually is at this moment. */
    max-height: calc(100dvh - 6vh);

    /* auto on top keeps it against the bottom; the inset keeps it clear of the
       home indicator on an installed iPhone, which a flat 0 did not. */
    margin: auto auto env(safe-area-inset-bottom);
  }
}

/* ---------------------------------------------------------------------------
   Focus
   ---------------------------------------------------------------------------
   Without this, focus falls to the browser default outline, which is
   near-invisible against Navy, Forest and Burnt. Keyed to :focus-visible so it
   appears for keyboard use and not on every tap. Two rings, light over dark, so
   it reads on any of the six palettes rather than only the one it was designed
   against. */
:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 25%, transparent);
  border-radius: 4px;
}
/* color-mix is recent; without it the outline alone still does the job. */
@supports not (color: color-mix(in srgb, red 50%, blue)) {
  :focus-visible { box-shadow: none; }
}

/* A status line for screen readers where the visible content is decorative
   shapes. Off-screen rather than display:none -- a hidden node is not read. */
.sr-status {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; padding: 0;
}

/* ---------------------------------------------------------------------------
   Motion
   ---------------------------------------------------------------------------
   Anything added to an app's own stylesheet should animate `transform` and
   `opacity` only. Those two belong to the compositor, so they cost no layout and
   no repaint and never touch the main thread -- which is what makes animation
   affordable on a phone at 8% battery in an airport. Animating width, height,
   top or margin runs layout every frame and is what makes a page feel hot in the
   hand. */

/* Reduced motion is not a nicety and not optional. Vestibular disorders make
   sweeping motion genuinely unpleasant, and the setting is how someone says so
   once rather than per app.
 *
 * Near-zero rather than `none` so animationend/transitionend still fire -- a
 * handler waiting for one would otherwise hang forever, which is the usual way
 * this well-meant block breaks a page. !important so it wins wherever an app's
 * own stylesheet puts its animations. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}
