/* ============================================================================
   tintus.digicam — shared design layer
   ----------------------------------------------------------------------------
   Apple-style fluid-interface primitives, translated to the web platform.

   Loaded by both shells:
     · frontend/templates/base.html               (storefront / public)
     · frontend/templates/admin/tintus_base.html  (admin + employee)

   Companion runtime: static/js/script.js  →  window.AD

   Design contract
   ---------------
   · Feedback lands on pointer-DOWN, never on release.
   · Anything a finger can grab is driven by a spring in JS (interruptible,
     velocity-aware). CSS transitions are reserved for hover/colour/focus —
     states a gesture never interrupts mid-flight.
   · Tracking and leading are size-specific. One letter-spacing for every size
     is wrong somewhere.
   · Chrome is a translucent layer content scrolls underneath, separated by a
     scroll-edge fade rather than a hard 1px rule.
   · Every motion has a non-vestibular equivalent under prefers-reduced-motion.

   Nothing here is decorative. Each value maps to a rule in that contract.
   ========================================================================== */

/* ============================================================================
   0. TYPEFACE — self-hosted
   ----------------------------------------------------------------------------
   Inter Variable with the optical-size axis, served from this origin instead of
   fonts.googleapis.com. Three reasons, in order of weight:

     1. The interface is Vietnamese. Self-hosting guarantees the `vietnamese`
        subset ships with the product; it carries ế ộ ữ ằ and the ₫ sign, and
        it is 15 KB. Nothing about the shop's typography depends on a third
        party being reachable.
     2. Two render-blocking round trips to two external origins (googleapis for
        the CSS, gstatic for the files) are removed from every page load.
     3. The subsets below are byte-identical to what Google served, so the
        rendering does not change.

   Declared as `Inter` — not `Inter Variable` — so every existing
   `font-family:'Inter'` in the templates keeps working untouched. Only the
   `normal` style is vendored: nothing in this product sets italic.
   Licence: SIL Open Font 1.1, see static/fonts/inter-LICENSE.txt
   ========================================================================== */

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url('../fonts/inter-vietnamese-opsz-normal.woff2') format('woff2-variations');
  unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url('../fonts/inter-latin-ext-opsz-normal.woff2') format('woff2-variations');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url('../fonts/inter-latin-opsz-normal.woff2') format('woff2-variations');
  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;
}

/* ============================================================================
   1. TOKENS
   ========================================================================== */

:root {
  /* --- Motion -------------------------------------------------------------
     Apple replaced mass/stiffness/damping with two designer parameters:
     damping ratio (overshoot) and response (time to reach target).
     CSS cannot spring, so these curves approximate a critically damped
     spring — the default for anything NOT gesture-driven. Gesture-driven
     motion uses the real spring integrator in script.js. */
  --ad-ease-out:    cubic-bezier(0.22, 1, 0.36, 1);      /* settle, damping 1.0 */
  --ad-ease-in:     cubic-bezier(0.64, 0, 0.78, 0);      /* exact mirror of above */
  --ad-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);      /* symmetric, reversible */
  --ad-ease-bounce: cubic-bezier(0.34, 1.32, 0.64, 1);   /* damping ~0.8 — momentum only */

  /* Response values matching the springs in script.js. */
  --ad-dur-press: 100ms;  /* press feedback — must feel instant */
  --ad-dur-fast:  180ms;  /* hover, colour, small state */
  --ad-dur-base:  300ms;  /* response 0.3 — drawers, sheets */
  --ad-dur-slow:  400ms;  /* response 0.4 — reposition, large surfaces */

  /* --- Brand -------------------------------------------------------------
     THE source of the brand ramp. It used to be declared three times — here,
     again as --accent in admin/tintus_base.html, and a third time as the
     `brand` palette handed to Tailwind — so one red had three definitions that
     could drift apart. Both of those now point at these values: the admin shell
     aliases --accent, and tailwind.{store,admin}.config.js emit
     var(--ad-brand-N) for every `brand-*` utility.

     Ratios against white, for the ones used as text: 700 = 7.06:1, 800 = 8.71:1.
     Do not introduce a brand text colour lighter than 600 without measuring. */
  --ad-brand-50:  #fef2f2;
  --ad-brand-100: #fee2e2;
  --ad-brand-200: #fecaca;
  --ad-brand-300: #fca5a5;
  --ad-brand-400: #f87171;
  --ad-brand-500: #ef4444;
  --ad-brand-600: #dc2626;
  --ad-brand-700: #b91c1c;
  --ad-brand-800: #991b1b;
  --ad-brand-900: #7f1d1d;

  --ad-accent:      var(--ad-brand-700);
  --ad-accent-soft: rgba(185, 28, 28, 0.12);
  --ad-accent-ring: rgba(185, 28, 28, 0.35);

  /* --- Materials ---------------------------------------------------------
     Weight encodes hierarchy: thick separates structural regions, thin draws
     attention to interactive elements. Never stack thin on thin. */
  --ad-mat-thin-bg:    rgba(255, 255, 255, 0.62);
  --ad-mat-thin-blur:  blur(20px) saturate(180%);
  --ad-mat-thick-bg:   rgba(250, 250, 251, 0.82);
  --ad-mat-thick-blur: blur(32px) saturate(180%);
  --ad-mat-dark-bg:    rgba(17, 24, 39, 0.72);
  --ad-mat-dark-blur:  blur(24px) saturate(170%);
  /* Bright top edge — light catching the lip of a real material. */
  --ad-mat-edge:       rgba(255, 255, 255, 0.55);

  /* --- Elevation ---------------------------------------------------------
     Bigger surfaces read as thicker: stronger blur AND a deeper shadow. */
  --ad-shadow-1: 0 1px 2px rgba(17, 24, 39, 0.04),
                 0 8px 24px -16px rgba(17, 24, 39, 0.12);
  --ad-shadow-2: 0 2px 4px rgba(17, 24, 39, 0.05),
                 0 18px 40px -18px rgba(17, 24, 39, 0.18);
  --ad-shadow-3: 0 4px 8px rgba(17, 24, 39, 0.06),
                 0 32px 64px -24px rgba(17, 24, 39, 0.26);
  --ad-shadow-sheet: 0 -2px 8px rgba(17, 24, 39, 0.04),
                     0 -24px 64px -16px rgba(17, 24, 39, 0.28);

  /* --- Composable transform channels ------------------------------------
     Press, hover-lift and drag each own a channel so they compose instead of
     clobbering one another. Transform-animated things write to a channel,
     never to `transform` directly. */
  --ad-scale: 1;
  --ad-lift: 0px;
}

/* ============================================================================
   2. OPTICAL TYPOGRAPHY
   ----------------------------------------------------------------------------
   Tracking is size-specific: large display text needs NEGATIVE tracking
   (letters drift apart as they grow), small text needs slightly positive
   tracking to stay legible. Leading tracks size inversely — tight on
   headings, looser on body.

   These hang off Tailwind's own size classes via :where(), so specificity
   stays at 0 and any explicit `tracking-*` / `leading-*` utility on the
   element still wins. Every page gets optical type for free.
   ========================================================================== */

body {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-optical-sizing: auto;
  font-variant-numeric: lining-nums;
}

/* Display sizes — tighten hard, lead tight. */
:where(.text-7xl) { letter-spacing: -0.028em; line-height: 1.02; }
:where(.text-6xl) { letter-spacing: -0.026em; line-height: 1.04; }
:where(.text-5xl) { letter-spacing: -0.024em; line-height: 1.06; }
:where(.text-4xl) { letter-spacing: -0.021em; line-height: 1.10; }
:where(.text-3xl) { letter-spacing: -0.018em; line-height: 1.16; }
:where(.text-2xl) { letter-spacing: -0.014em; line-height: 1.22; }
:where(.text-xl)  { letter-spacing: -0.011em; line-height: 1.34; }
:where(.text-lg)  { letter-spacing: -0.007em; line-height: 1.44; }
/* Body sits at zero — the size the face was drawn for. */
:where(.text-base){ letter-spacing: 0;        line-height: 1.55; }
:where(.text-sm)  { letter-spacing: 0.002em;  line-height: 1.55; }
/* Small text opens up rather than closing down. */
:where(.text-xs)  { letter-spacing: 0.008em;  line-height: 1.5; }
:where(.text-\[0\.72rem\]),
:where(.text-\[0\.7rem\]),
:where(.text-\[0\.65rem\]),
:where(.text-\[0\.6rem\]) { letter-spacing: 0.014em; }

/* Heavy weights at display size need extra tightening — the strokes already
   fill the space the tracking was reserving. */
:where(.font-black, .font-extrabold):where(.text-3xl, .text-4xl, .text-5xl, .text-6xl, .text-7xl) {
  letter-spacing: -0.03em;
}

/* Figures in tables and money fields must align vertically. */
:where(table) td, :where(table) th,
.ad-tnum, .ta-count, .cam-money, .cc-money, .rd-money, .money-live {
  font-variant-numeric: lining-nums tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* Tracked uppercase micro-labels: the one place positive tracking is right. */
:where(.uppercase):where(.text-xs, .text-\[0\.7rem\], .text-\[0\.65rem\], .text-\[0\.6rem\]) {
  font-variant-numeric: lining-nums tabular-nums;
}

/* ============================================================================
   3. RESPONSE — feedback on pointer-down
   ----------------------------------------------------------------------------
   "The moment lag appears, the feeling of directness falls off a cliff."

   `data-ad-pressed` is set by script.js on pointerdown — instant, and it works
   on touch where :active is both delayed and unreliable.
   ========================================================================== */

/* The `body ` prefix is load-bearing, not decoration. Tailwind's own
   `transition-colors` / `transition-all` utilities are single-class selectors
   injected by the CDN after this file, so a bare `.ad-press` would lose the
   cascade and the press scale would snap instead of settling. One extra
   element in the selector outranks them without reaching for !important. */
body .ad-press {
  transform: translateY(var(--ad-lift)) scale(var(--ad-scale));
  transition: transform var(--ad-dur-press) var(--ad-ease-out),
              box-shadow var(--ad-dur-fast) var(--ad-ease-out),
              background-color var(--ad-dur-fast) var(--ad-ease-out),
              border-color var(--ad-dur-fast) var(--ad-ease-out),
              color var(--ad-dur-fast) var(--ad-ease-out);
  /* Kill the ~300ms tap delay and iOS's grey flash. */
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

/* A surface that responds to hover but is NOT a tap target. Replaces
   `transition-all`, which also animates layout properties. */
body .ad-hoverable {
  transition: box-shadow var(--ad-dur-base) var(--ad-ease-out),
              border-color var(--ad-dur-base) var(--ad-ease-out),
              background-color var(--ad-dur-base) var(--ad-ease-out),
              color var(--ad-dur-base) var(--ad-ease-out);
}

.ad-press[data-ad-pressed]    { --ad-scale: 0.97; }
/* Large surfaces travel less — the same 3% reads as a lurch on a big card. */
.ad-press-lg[data-ad-pressed] { --ad-scale: 0.985; }
/* Small chips and icon buttons can take more. */
.ad-press-sm[data-ad-pressed] { --ad-scale: 0.94; }

.ad-press:disabled,
.ad-press[aria-disabled="true"] { --ad-scale: 1; }

/* A forgiving ~10px target around small controls, without changing layout. */
.ad-tap { position: relative; }
.ad-tap::before { content: ''; position: absolute; inset: -10px; }

/* ============================================================================
   4. FOCUS — one treatment everywhere
   ========================================================================== */

:focus-visible {
  outline: 2px solid var(--ad-accent);
  outline-offset: 2px;
}
:focus:not(:focus-visible) { outline: none; }

/* `body ` prefix so this outranks Tailwind's `focus:border-*` utilities — see
   the note on .ad-press. One focus treatment for every field on every page,
   regardless of which utilities that page happens to use. */
body .ad-field:focus {
  outline: none;
  border-color: var(--ad-accent);
  box-shadow: 0 0 0 3px var(--ad-accent-soft);
}

/* ============================================================================
   5. MATERIALS & DEPTH
   ----------------------------------------------------------------------------
   Translucent layers are a floating functional plane: structure without
   stealing focus, with content scrolling underneath.
   ========================================================================== */

.ad-material {
  background: var(--ad-mat-thin-bg);
  -webkit-backdrop-filter: var(--ad-mat-thin-blur);
  backdrop-filter: var(--ad-mat-thin-blur);
}
.ad-material-thick {
  background: var(--ad-mat-thick-bg);
  -webkit-backdrop-filter: var(--ad-mat-thick-blur);
  backdrop-filter: var(--ad-mat-thick-blur);
}
.ad-material-dark {
  background: var(--ad-mat-dark-bg);
  -webkit-backdrop-filter: var(--ad-mat-dark-blur);
  backdrop-filter: var(--ad-mat-dark-blur);
  color: #fff;
}
/* The lit lip of a material catching light from above. */
.ad-material-edge { border-top: 1px solid var(--ad-mat-edge); }

/* Vibrancy — text over a translucent, shifting backdrop needs more contrast
   and a touch more weight. Flat grey disappears the moment something dark
   scrolls underneath it. */
.ad-vibrant       { color: #1f2937; font-weight: 600; letter-spacing: 0.004em; }
.ad-vibrant-muted { color: #4b5563; letter-spacing: 0.006em; }

.ad-elev-1 { box-shadow: var(--ad-shadow-1); }
.ad-elev-2 { box-shadow: var(--ad-shadow-2); }
.ad-elev-3 { box-shadow: var(--ad-shadow-3); }

/* Legacy alias — existing markup keeps its elevation. */
.ring-soft { box-shadow: var(--ad-shadow-1); }

/* ============================================================================
   6. SCROLL EDGE EFFECTS
   ----------------------------------------------------------------------------
   A 1px divider under floating chrome is a hard seam. Fade content into the
   chrome instead — and only once content is actually underneath it.
   ========================================================================== */

.ad-scroll-edge {
  border-bottom: 1px solid transparent;
  transition: border-color var(--ad-dur-base) var(--ad-ease-out),
              box-shadow var(--ad-dur-base) var(--ad-ease-out);
}
.ad-scroll-edge::after {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 100%;
  height: 20px;
  pointer-events: none;
  background: linear-gradient(to bottom,
              rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0));
  opacity: 0;
  transition: opacity var(--ad-dur-base) var(--ad-ease-out);
}
.ad-scroll-edge[data-ad-scrolled]::after { opacity: 1; }
.ad-scroll-edge[data-ad-scrolled] {
  border-bottom-color: rgba(17, 24, 39, 0.06);
  box-shadow: 0 1px 12px -4px rgba(17, 24, 39, 0.10);
}

/* ============================================================================
   7. SHEETS, SCRIMS & POPOVERS
   ----------------------------------------------------------------------------
   Modal task  → dim the scrim and push the background back (focus).
   Parallel panel → translucency and offset, NO scrim (keep the flow).
   Position/scale are owned by springs in script.js; CSS only sets the stage.
   ========================================================================== */

.ad-scrim {
  position: fixed;
  inset: 0;
  z-index: 1000;
  background: rgba(17, 24, 39, 0.42);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
  opacity: 0;
  will-change: opacity;
  /* Opacity is driven by the sheet's drag progress — a transition here would
     fight the gesture. */
}
.ad-scrim[hidden] { display: none; }

/* Parent layers get pushed back behind a modal task. Stacked sheets dim
   progressively via --ad-depth. */
.ad-pushed {
  transform: scale(calc(1 - 0.012 * var(--ad-depth, 1)));
  filter: brightness(calc(1 - 0.03 * var(--ad-depth, 1)));
  transition: transform var(--ad-dur-slow) var(--ad-ease-out),
              filter var(--ad-dur-slow) var(--ad-ease-out);
  transform-origin: center top;
}

.ad-sheet {
  position: fixed;
  z-index: 1001;
  background: #fff;
  box-shadow: var(--ad-shadow-sheet);
  will-change: transform;
  /* Transform is composed from independent channels so the drag offset
     (--ad-dy) never clobbers the layout offset (--ad-sx/--ad-sy) or the
     arrival scale. No transition here: the spring in script.js owns transform
     so the sheet can be grabbed and reversed mid-flight. */
  transform: translate3d(var(--ad-sx, 0px),
                         calc(var(--ad-sy, 0px) + var(--ad-dy, 0px)), 0)
             scale(var(--ad-sscale, 1));
}
.ad-sheet[hidden] { display: none; }

.ad-sheet-bottom {
  left: 0; right: 0; bottom: 0;
  max-height: 92vh;
  border-radius: 22px 22px 0 0;
  display: flex;
  flex-direction: column;
}
/* Centred dialog on wide viewports — same element, different geometry, so the
   open/close gesture and its springs are shared rather than reimplemented. */
@media (min-width: 640px) {
  .ad-sheet-center {
    --ad-sx: -50%;
    --ad-sy: -50%;
    left: 50%; top: 50%; right: auto; bottom: auto;
    width: min(92vw, 560px);
    max-height: 86vh;
    border-radius: 20px;
    display: flex;
    flex-direction: column;
  }
  .ad-sheet-center.ad-sheet-wide  { width: min(94vw, 880px); }
  .ad-sheet-center.ad-sheet-xwide { width: min(96vw, 1120px); }
}

.ad-sheet-grabber {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 10px 0 6px;
  cursor: grab;
  touch-action: none;   /* we own this gesture; the browser must not scroll */
}
.ad-sheet-grabber:active { cursor: grabbing; }
.ad-sheet-grabber::after {
  content: '';
  width: 38px; height: 5px;
  border-radius: 999px;
  background: rgba(17, 24, 39, 0.18);
  transition: background-color var(--ad-dur-fast) var(--ad-ease-out);
}
.ad-sheet-grabber[data-ad-pressed]::after { background: rgba(17, 24, 39, 0.34); }

.ad-sheet-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

/* The panel inside a legacy admin overlay (see the modal adapter in
   script.js). Same composed-channel approach as .ad-sheet: the drag offset and
   the arrival scale occupy separate channels so neither clobbers the other, and
   no transition — the spring owns transform so the dialog stays grabbable. */
body .ad-modal-panel {
  transform: translate3d(0, var(--ad-dy, 0px), 0) scale(var(--ad-sscale, 1));
  will-change: transform;
}
/* Holds a legacy dialog visible for the length of its exit animation. The page
   has already set `display:none` inline by this point, so overriding it needs
   !important — this is the one place in the layer that earns it. Dropped again
   the moment the exit spring settles (see the modal adapter in script.js). */
body .ad-modal-exiting { display: flex !important; }

/* The header is the grab handle; make that legible without a visual change. */
body .ad-modal-panel [data-ad-drag] { cursor: grab; touch-action: none; }
body .ad-modal-panel [data-ad-drag]:active { cursor: grabbing; }
/* Text selection during a drag looks broken and fights the gesture. */
body .ad-modal-panel [data-ad-drag] > h3,
body .ad-modal-panel [data-ad-drag] > h4 { user-select: none; }

@media (prefers-reduced-motion: reduce) {
  body .ad-modal-panel { transform: none !important; }
  body .ad-modal-panel [data-ad-drag] { cursor: default; }
}

/* Popover — scales from the element that triggered it, not from its own
   centre, so the spatial relationship between button and content is legible.
   script.js writes --ad-origin-x / -y from the trigger's geometry. */
.ad-popover {
  transform-origin: var(--ad-origin-x, 50%) var(--ad-origin-y, 0%);
}
/* will-change is a hint for motion that is about to happen, not a permanent
   property. Nav dropdown panels stay in the DOM at visibility:hidden, so
   declaring it on the base rule kept a compositor layer alive for every menu on
   every page, forever. script.js sets this attribute for the length of the
   animation and drops it on rest. */
.ad-popover[data-ad-live] { will-change: transform, opacity; }

/* Materialize, don't just fade: blur and scale arrive together so the surface
   reads as a real material appearing rather than an image cross-fading. */
.ad-materialize-enter {
  animation: ad-materialize var(--ad-dur-base) var(--ad-ease-out) both;
}
@keyframes ad-materialize {
  from { opacity: 0; transform: scale(0.94); }
  to   { opacity: 1; transform: scale(1); }
}

/* ============================================================================
   8. DISCLOSURE
   ========================================================================== */

.ad-disclosure { overflow: hidden; }

.ad-chevron, .chevron {
  transition: transform var(--ad-dur-base) var(--ad-ease-out);
}
details[open] > summary .ad-chevron,
details[open] > summary .chevron,
.ad-open > .ad-chevron { transform: rotate(180deg); }

details > summary { list-style: none; cursor: pointer; }
details > summary::-webkit-details-marker { display: none; }

/* ============================================================================
   9. COMPONENT UPGRADES
   ----------------------------------------------------------------------------
   Existing markup, re-pointed at the transform channels so hover-lift and
   press-scale compose instead of overwriting each other.
   ========================================================================== */

/* --- Storefront product card -------------------------------------------- */
body .product-card {
  transform: translateY(var(--ad-lift)) scale(var(--ad-scale));
  transition: transform var(--ad-dur-base) var(--ad-ease-out),
              box-shadow var(--ad-dur-base) var(--ad-ease-out),
              border-color var(--ad-dur-base) var(--ad-ease-out);
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
/* The press channel is faster than the hover-lift channel: a press must feel
   immediate, a lift may take its time. */
body .product-card[data-ad-pressed] {
  transition: transform var(--ad-dur-press) var(--ad-ease-out),
              box-shadow var(--ad-dur-base) var(--ad-ease-out),
              border-color var(--ad-dur-base) var(--ad-ease-out);
}
/* Hover-lift only where there is a real pointer — on touch it fires on tap
   and reads as a glitch. */
@media (hover: hover) and (pointer: fine) {
  .product-card:hover {
    --ad-lift: -5px;
    box-shadow: var(--ad-shadow-3);
    border-color: var(--ad-accent-ring);
  }
}
/* Pressing a lifted card settles it toward the surface — the press reads as
   the card being pushed, not shrunk in mid-air. */
.product-card[data-ad-pressed] { --ad-scale: 0.985; --ad-lift: -1px; }

/* --- Storefront hero backdrop -------------------------------------------
   Replaces a 1920px stock photo that was held at opacity .2 under a
   96%-opaque gradient: a third-party request in the first viewport, for
   something that resolved to almost flat black by the time it composited.

   Two layers do the same job with nothing to download. The bloom carries the
   warm shift toward the right that the old gradient established; the grain is
   a fine diagonal hatch that keeps a large dark field from banding on 8-bit
   panels, masked so it only shows where the bloom gives it something to sit
   on. Values are matched to the gradient they replace, not invented. */
.hero-bloom {
  background:
    radial-gradient(120% 90% at 88% 16%, rgba(153, 27, 27, 0.30), transparent 62%),
    radial-gradient(72% 64% at 10% 104%, rgba(69, 10, 10, 0.24), transparent 72%),
    linear-gradient(105deg, #08080a 45%, #200c0c 100%);
}
.hero-grain {
  background-image: repeating-linear-gradient(
    115deg,
    rgba(255, 255, 255, 0.018) 0 1px,
    transparent 1px 3px
  );
  -webkit-mask-image: radial-gradient(120% 90% at 88% 16%, #000 20%, transparent 70%);
          mask-image: radial-gradient(120% 90% at 88% 16%, #000 20%, transparent 70%);
}

/* --- Pills / badges ----------------------------------------------------- */
.pill {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 11px;
  border-radius: 999px;
  font-size: 0.65rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-variant-numeric: lining-nums tabular-nums;
}
/* These render at 0.65rem/800 — small text, so AA wants 4.5:1, not 3:1. The
   previous trio measured 4.32 / 4.53 / 4.36; one step darker on each foreground
   clears it with margin while keeping the same tinted-chip read.
   Measured against their own backgrounds: 6.06 / 6.36 / 6.86:1. */
.pill-available { background: #dcfce7; color: #166534; }
.pill-reserved  { background: #fef3c7; color: #92400e; }
.pill-out       { background: #f3f4f6; color: #4b5563; }

/* --- Admin shell (tintus_base) ------------------------------------------ */
#tintus-app .ta-navitem,
#tintus-app .ta-subitem {
  transform: scale(var(--ad-scale));
  transition: background-color var(--ad-dur-fast) var(--ad-ease-out),
              color var(--ad-dur-fast) var(--ad-ease-out),
              box-shadow var(--ad-dur-fast) var(--ad-ease-out),
              transform var(--ad-dur-press) var(--ad-ease-out);
}
#tintus-app .ta-navitem[data-ad-pressed],
#tintus-app .ta-subitem[data-ad-pressed] { --ad-scale: 0.975; }

/* The sidebar is a structural region → heavier material.
   The topbar floats over content → lighter one. */
#tintus-app .ta-sidebar {
  background: var(--ad-mat-thick-bg);
  -webkit-backdrop-filter: var(--ad-mat-thick-blur);
  backdrop-filter: var(--ad-mat-thick-blur);
}
#tintus-app .ta-topbar {
  background: var(--ad-mat-thin-bg);
  -webkit-backdrop-filter: var(--ad-mat-thin-blur);
  backdrop-filter: var(--ad-mat-thin-blur);
  border-bottom: 1px solid transparent;
  transition: border-color var(--ad-dur-base) var(--ad-ease-out),
              box-shadow var(--ad-dur-base) var(--ad-ease-out);
}
#tintus-app .ta-topbar[data-ad-scrolled] {
  border-bottom-color: rgba(17, 24, 39, 0.07);
  box-shadow: 0 1px 12px -4px rgba(17, 24, 39, 0.10);
}

/* The sub-nav reveal is spring-driven in JS; drop the max-height guess so the
   two don't fight. The `open` rule stays as the no-JS fallback. */
#tintus-app .ta-subnav { transition: none; }
#tintus-app .ta-group.open .ta-subnav { max-height: none; }

/* --- Toasts -------------------------------------------------------------
   Enter and exit along the SAME path (in from the right, out to the right),
   with mirrored easing so the return traces the arrival. */
.tintus-toast, .ad-toast {
  background: #111827;
  color: #fff;
  padding: 11px 16px;
  border-radius: 12px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.004em;
  box-shadow: var(--ad-shadow-3);
  opacity: 0;
  transform: translate3d(20px, 0, 0) scale(0.96);
  transition: opacity var(--ad-dur-base) var(--ad-ease-in),
              transform var(--ad-dur-base) var(--ad-ease-in);
  max-width: 340px;
  will-change: transform, opacity;
}
.tintus-toast.show, .ad-toast.show {
  opacity: 1;
  transform: translate3d(0, 0, 0) scale(1);
  transition: opacity var(--ad-dur-base) var(--ad-ease-out),
              transform var(--ad-dur-base) var(--ad-ease-bounce);
}
/* --- In-flight progress bar ---------------------------------------------
   Acknowledges a request that is taking long enough to notice. It only appears
   after 200ms (see AD.progress), so quick saves stay silent — a bar that
   flickers on every keystroke-save would be noise, not feedback.
   `linear` is correct here: this is constant indeterminate motion, not an
   element entering or settling, so an ease would read as stuttering. */
.ad-progress {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 2px;
  z-index: 10000;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--ad-dur-fast) var(--ad-ease-out);
  background: linear-gradient(90deg,
              transparent, var(--ad-accent) 40%, var(--ad-accent) 60%, transparent);
  background-size: 45% 100%;
  background-repeat: no-repeat;
}
.ad-progress[data-ad-on] {
  opacity: 1;
  animation: ad-progress-slide 1.1s linear infinite;
}
@keyframes ad-progress-slide {
  from { background-position: -45% 0; }
  to   { background-position: 145% 0; }
}
/* Reduced motion keeps the acknowledgement but drops the travel: a static bar
   still says "working", which is the information the animation carried. */
@media (prefers-reduced-motion: reduce) {
  .ad-progress[data-ad-on] {
    animation: none;
    background: var(--ad-accent);
    background-size: 100% 100%;
    opacity: 0.85;
  }
}

.tintus-toast.ok   { background: #065f46; }
.tintus-toast.err  { background: #991b1b; }
.tintus-toast.warn { background: #92400e; }

/* --- Tables ------------------------------------------------------------- */
#tintus-app tbody tr {
  transition: background-color var(--ad-dur-fast) var(--ad-ease-out);
}
#tintus-app tbody tr[data-ad-pressed] { background-color: rgba(17, 24, 39, 0.035); }

/* A sticky table header is floating chrome: it earns its separation only once
   rows have actually scrolled beneath it. The permanent 1px rule these tables
   shipped with reads as a seam even on an unscrolled table. */
.ad-scroll-host thead th {
  transition: box-shadow var(--ad-dur-base) var(--ad-ease-out),
              border-bottom-color var(--ad-dur-base) var(--ad-ease-out);
}
.ad-scroll-host:not([data-ad-scrolled]) thead th {
  border-bottom-color: transparent;
}
.ad-scroll-host[data-ad-scrolled] thead th {
  box-shadow: 0 6px 12px -8px rgba(17, 24, 39, 0.28);
}

.ad-overscroll { overscroll-behavior: contain; }

/* --- Wide tables: a scroll box instead of a taller page ------------------
   A wide table used to grow the PAGE rather than scroll inside itself, which
   put the horizontal scrollbar under the last row: to move sideways you first
   had to scroll past fifty rows to reach the control that moves you sideways.

   The table now scrolls inside a box sized to what is left of the viewport,
   so the bar sits at the bottom of the box — on screen, always. The header
   stays put and the first column pins to the left, because a table you can
   scroll sideways is a table where "which row is this?" stops being obvious.

   Height: --ad-table-h is set per host by AD.fitTables() (script.js), which
   measures how much chrome sits above it. The calc() here is the fallback if
   that never runs, and max() keeps the box from collapsing on a short window.

       <div class="ad-scroll-host ad-table-host"> <table>…</table> </div>
*/
.ad-table-host {
  overflow: auto;
  max-height: max(320px, var(--ad-table-h, calc(100vh - 300px)));
}

.ad-table-host thead th {
  position: sticky;
  top: 0;
  z-index: 20;
  /* Sticky chrome needs its own opaque fill — the row it belonged to no longer
     paints behind it once it detaches. */
  background-color: #f9fafb;
  border-bottom: 1px solid #e5e7eb;
}

/* The pinned first column. `:not(:last-child)` excludes a lone full-width cell:
   an expandable detail row (colspan) or an empty state is one <td>, and pinning
   THAT would freeze the whole row against the left edge. */
.ad-table-host thead th:first-child:not(:last-child) {
  left: 0;
  z-index: 30;
}
.ad-table-host tbody td:first-child:not(:last-child) {
  position: sticky;
  left: 0;
  z-index: 10;
  /* Opaque, and it has to be: columns slide UNDER this cell, so a translucent
     fill (every Tailwind row tint here is one — bg-blue-50/40, bg-red-50/60)
     would let them show through as it passes. `background-color: inherit` is
     the tempting one-liner and it inherits exactly that translucency.

     So the row hands its colour down as a variable instead, already flattened
     against white. Rows with no tint get the default and never think about it. */
  background-color: var(--ad-row-bg, #fff);
  border-right: 1px solid #e5e7eb;
}
/* Hover lives on the <tr> as bg-gray-50/60; this is that colour over white, so
   the pinned cell lights up with the rest of its row instead of staying pale. */
.ad-table-host tbody tr:hover td:first-child:not(:last-child) {
  background-color: var(--ad-row-bg-hover, #fbfcfd);
}

/* A totals row is the answer the table exists to give. Inside a scroll box it
   would sit below fifty rows, so it pins to the bottom the way the header pins
   to the top. */
.ad-table-host tfoot td,
.ad-table-host tfoot th {
  position: sticky;
  bottom: 0;
  z-index: 20;
  background-color: #fff;
  border-top: 1px solid #e5e7eb;
}

/* Windows Chrome defaults to OVERLAY scrollbars: they take no layout space and
   fade out when idle, so a table with six columns off to the right looks exactly
   like one with nothing hidden. Measured in the browser — the probe reports 0px
   of scrollbar space — so this is not a hypothetical.

   Fade the right edge while there is more to reach, and drop it at the end. The
   card that wraps the box already clips and rounds, so the fade lands inside it;
   :has() picks that card out without a class in twenty-four templates. */
#tintus-app *:has(> .ad-table-host) { position: relative; }
#tintus-app *:has(> .ad-table-host)::after {
  content: '';
  position: absolute;
  top: 1px;
  bottom: 1px;
  right: 0;
  width: 36px;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--ad-dur-base) var(--ad-ease-out);
  /* A white-to-transparent fade is the usual choice and it is invisible here —
     checked in the browser: white veil, white table. Darken instead, in the same
     ink the pinned column's shadow uses, so the two edges read as one idea. */
  background: linear-gradient(to right,
              rgba(17, 24, 39, 0), rgba(17, 24, 39, 0.11));
}
#tintus-app *:has(> .ad-table-host[data-ad-more-right])::after { opacity: 1; }

/* The pinned column earns its edge the same way the header does: only once
   there is something hidden underneath it. */
.ad-table-host thead th:first-child:not(:last-child),
.ad-table-host tbody td:first-child:not(:last-child) {
  transition: border-right-color var(--ad-dur-base) var(--ad-ease-out),
              box-shadow var(--ad-dur-base) var(--ad-ease-out);
}
.ad-table-host:not([data-ad-scrolled-x]) tbody td:first-child:not(:last-child) {
  border-right-color: transparent;
}
.ad-table-host[data-ad-scrolled-x] tbody td:first-child:not(:last-child),
.ad-table-host[data-ad-scrolled-x] thead th:first-child:not(:last-child) {
  box-shadow: 6px 0 12px -8px rgba(17, 24, 39, 0.28);
}


/* ============================================================================
   10. ACCESSIBILITY
   ----------------------------------------------------------------------------
   Reduced motion is not "no feedback" — it's a gentler, non-vestibular
   equivalent. Opacity and colour survive; travel and overshoot do not.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  :root {
    --ad-ease-bounce: var(--ad-ease-out);   /* no overshoot, anywhere */
    --ad-dur-base: 160ms;
    --ad-dur-slow: 200ms;
  }

  /* Travel and lift go away; press feedback becomes an opacity change so the
     interaction still confirms itself. */
  .product-card:hover,
  .product-card[data-ad-pressed] { --ad-lift: 0px; --ad-scale: 1; }
  .ad-press[data-ad-pressed],
  .ad-press-lg[data-ad-pressed],
  .ad-press-sm[data-ad-pressed] { --ad-scale: 1; opacity: 0.72; }

  .ad-pushed { transform: none; }

  /* Sheets cross-fade in place rather than sliding. script.js switches its
     springs to an opacity path — this is the CSS half of that. */
  .ad-sheet {
    transform: none !important;
    transition: opacity var(--ad-dur-base) var(--ad-ease-out);
  }
  .ad-sheet-grabber { display: none; }

  .tintus-toast, .ad-toast {
    transform: none !important;
    transition: opacity var(--ad-dur-base) var(--ad-ease-out);
  }

  .ad-materialize-enter { animation: none; opacity: 1; }

  /* Kill decorative loops — a slow oscillation is the worst offender. */
  .animate-pulse, .animate-spin, .animate-bounce { animation: none !important; }

  /* Tailwind's decorative hover/group-hover zooms (the emoji that grows when
     you hover a card) are pure ornament — exactly what reduced motion asks us
     to drop. The colour change beside them carries the same information. */
  .group:hover [class*="group-hover:scale-"],
  [class*="hover:scale-"]:hover,
  [class*="hover:-translate"]:hover,
  [class*="hover:translate"]:hover { transform: none !important; }

  html { scroll-behavior: auto; }
}

@media (prefers-reduced-transparency: reduce) {
  :root {
    --ad-mat-thin-bg:  #ffffff;
    --ad-mat-thick-bg: #f9fafb;
    --ad-mat-dark-bg:  #111827;
    --ad-mat-thin-blur:  none;
    --ad-mat-thick-blur: none;
    --ad-mat-dark-blur:  none;
  }
  .ad-material, .ad-material-thick, .ad-material-dark,
  #tintus-app .ta-sidebar, #tintus-app .ta-topbar {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  .ad-scrim {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: rgba(17, 24, 39, 0.6);
  }
  .ad-scroll-edge::after { display: none; }
}

@media (prefers-contrast: more) {
  :root {
    --ad-mat-thin-bg:  #ffffff;
    --ad-mat-thick-bg: #ffffff;
    --ad-mat-thin-blur:  none;
    --ad-mat-thick-blur: none;
  }
  .ad-material, .ad-material-thick,
  #tintus-app .ta-sidebar, #tintus-app .ta-topbar,
  .ad-sheet, .ad-popover {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    border: 1px solid #374151;
  }
  .ad-vibrant, .ad-vibrant-muted { color: #111827; font-weight: 650; }
  :focus-visible { outline-width: 3px; }
}

@media print {
  .ad-material, .ad-material-thick, .ad-material-dark {
    background: #fff !important;
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
  }
  .ad-scrim, .ad-sheet, .tintus-toast, #tintus-toast-wrap { display: none !important; }
}
