/* ─────────────────────────────────────────────────────────────────────────
   Shared interaction layer — EDSOL Engineering Systems Ltd

   Loaded by every page after the Tailwind CDN so it can correct the one
   thing Tailwind's utilities get wrong here: pages use `focus:outline-none`
   on inputs to drop the browser's default ring, but never replace it. That
   leaves keyboard users with no visible position. Everything below restores
   a consistent, brand-coloured focus state and adds the press/hover feedback
   that makes controls feel responsive.
   ───────────────────────────────────────────────────────────────────────── */

:root {
  --ui-ring: #2f52a0;
  --ui-ring-soft: rgba(47, 82, 160, 0.28);
  /* Stronger than the built-in CSS easings, which are too soft to read as
     intentional. Enter/exit uses ease-out so movement starts immediately. */
  --ui-ease: cubic-bezier(0.23, 1, 0.32, 1);
  --ui-ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
}

/* ── Focus ────────────────────────────────────────────────────────────────
   :focus-visible only, so a mouse click never shows a ring but Tab always
   does.

   !important is deliberate here. Pages apply Tailwind's `focus:outline-none`
   to strip the default ring, and `.focus\:outline-none:focus` (0,2,0) outranks
   `input:focus-visible` (0,1,1) whatever the source order. This file exists
   purely to put that ring back, so it has to win.                           */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--ui-ring) !important;
  outline-offset: 2px !important;
}

/* Fields sit inside cards, so tighten the offset and add a soft halo that
   reads at a glance without shifting layout. */
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline-offset: 1px !important;
  box-shadow: 0 0 0 4px var(--ui-ring-soft) !important;
}

/* On the dark contact panel and footer the blue ring disappears — invert. */
.bg-\[var\(--brand-green-deep\)\] a:focus-visible,
.bg-\[var\(--brand-green-deep\)\] button:focus-visible,
[style*="brand-green-deep"] a:focus-visible,
[style*="brand-green-deep"] button:focus-visible {
  outline-color: #f4991f;
}

/* ── Press feedback ───────────────────────────────────────────────────────
   A button that doesn't move on press feels dead. Kept at 0.97 so it reads
   as a press, not a bounce.                                                */
button:not(:disabled):active,
a[class*="rounded"]:active {
  transform: scale(0.97);
}
button,
a[class*="rounded"] {
  transition: transform 0.12s var(--ui-ease);
}

button:disabled {
  cursor: not-allowed;
  opacity: 0.55;
}

/* ── Dialogs ──────────────────────────────────────────────────────────────
   25 modals across the app are shown by dropping Tailwind's `hidden` class,
   which meant they appeared instantly. Elements that pop in without any
   transition read as broken, so both the scrim and the panel now fade in.

   Matched structurally (a full-screen z-50 flex-centred overlay) rather than
   by adding a class to every dialog, so nothing had to change in the markup.
   The mobile sidebar scrim is z-40 and is deliberately excluded.

   Keyframes rather than transitions is the right call here: dialogs open
   occasionally, never in rapid succession, so there is no interruption to
   retarget, and keyframes work off a plain class toggle with no JS hooks. */
/* A dialog taller than the window must stay reachable. These overlays centre
   their panel with `items-center`, and a flex item centred inside a scroll
   container has its overflow *above* the top edge clipped with no way to
   scroll back up to it: on a short screen the dialog's heading and first
   fields simply cannot be reached.

   Aligning to the start and centring via auto margins instead behaves the
   same when the panel fits, and top-aligns and scrolls properly when it does
   not. Fixed here so it applies to every dialog rather than one at a time. */
.fixed.inset-0.z-50.items-center.justify-center {
  align-items: flex-start;
  overflow-y: auto;
}
.fixed.inset-0.z-50.items-center.justify-center > * {
  margin-top: auto;
  margin-bottom: auto;
  flex-shrink: 0;
}

.fixed.inset-0.z-50.items-center.justify-center:not(.hidden) {
  animation: dialog-scrim-in 160ms var(--ui-ease);
}
.fixed.inset-0.z-50.items-center.justify-center:not(.hidden) > * {
  animation: dialog-panel-in 200ms var(--ui-ease);
}
@keyframes dialog-scrim-in {
  from { opacity: 0; }
}
/* Starts at 0.96, never 0 — nothing in the real world appears from nothing.
   Origin stays centred: a modal isn't anchored to a trigger. */
@keyframes dialog-panel-in {
  from { opacity: 0; transform: scale(0.96) translateY(8px); }
}

/* Slow pulse on the breakdown line. Looping motion is normally decorative
   noise, but here it is doing a job: flagging the one route a plant with a
   stopped line needs to find fast. Suppressed under reduced motion below. */
@keyframes ping-slow {
  75%, 100% { transform: scale(2.2); opacity: 0; }
}

/* Mobile nav drops down from behind the header, so it enters from above. */
#mobile-menu:not(.hidden) {
  animation: menu-in 180ms var(--ui-ease);
}
@keyframes menu-in {
  from { opacity: 0; transform: translateY(-8px); }
}

/* ── Hover lift ───────────────────────────────────────────────────────────
   Gated behind a real pointer: on touch, hover fires on tap and leaves the
   card stuck in its lifted state. */
@media (hover: hover) and (pointer: fine) {
  .card-hover:hover { transform: translateY(-4px); }
}

/* ── Per-area colour ──────────────────────────────────────────────────────
   Every working area owns a hue, the way Gmail and Calendar feel like
   different rooms in the same building. The point is orientation: you should
   know which part of the system you are in from the corner of your eye,
   without reading the heading.

   Only the chrome takes the hue — the header wash, the page icon, the active
   chip, the accent rule. Brand blue and amber keep the primary buttons, so
   nothing here loosens the brand or leaks into a quote or invoice PDF.

   Warm bases throughout: these are near-whites with a little red in them
   rather than the blue-grey default, so a screen someone stares at all day
   feels lit rather than fluorescent.                                       */
:root {
  --accent:      #2f52a0;
  --accent-ink:  #1e3a72;
  --accent-soft: #eef3fb;
  --accent-wash: #f7f9fd;
}

body[data-page="dashboard"]        { --accent:#2f52a0; --accent-ink:#1e3a72; --accent-soft:#e8eefb; --accent-wash:#f6f8fd; }
body[data-page="projects"]         { --accent:#0f766e; --accent-ink:#115e59; --accent-soft:#d9f2ef; --accent-wash:#f4fbfa; }
body[data-page="project"]          { --accent:#0f766e; --accent-ink:#115e59; --accent-soft:#d9f2ef; --accent-wash:#f4fbfa; }
body[data-page="job-cards"]        { --accent:#6d28d9; --accent-ink:#5b21b6; --accent-soft:#ebe6fd; --accent-wash:#f8f6fe; }
body[data-page="contracts"]        { --accent:#3730a3; --accent-ink:#312e81; --accent-soft:#e6e5fb; --accent-wash:#f6f6fd; }
body[data-page="quotes"]           { --accent:#b45309; --accent-ink:#92400e; --accent-soft:#fdeed6; --accent-wash:#fdf9f3; }
body[data-page="new-quote"]        { --accent:#b45309; --accent-ink:#92400e; --accent-soft:#fdeed6; --accent-wash:#fdf9f3; }
body[data-page="quote"]            { --accent:#b45309; --accent-ink:#92400e; --accent-soft:#fdeed6; --accent-wash:#fdf9f3; }
body[data-page="invoices"]         { --accent:#be123c; --accent-ink:#9f1239; --accent-soft:#fde3e9; --accent-wash:#fdf6f7; }
body[data-page="invoice"]          { --accent:#be123c; --accent-ink:#9f1239; --accent-soft:#fde3e9; --accent-wash:#fdf6f7; }
body[data-page="new-sale"]         { --accent:#be123c; --accent-ink:#9f1239; --accent-soft:#fde3e9; --accent-wash:#fdf6f7; }
body[data-page="sales"]            { --accent:#c2410c; --accent-ink:#9a3412; --accent-soft:#fde7d9; --accent-wash:#fdf8f4; }
body[data-page="customers"]        { --accent:#0369a1; --accent-ink:#075985; --accent-soft:#dbeefc; --accent-wash:#f5fafd; }
body[data-page="customer-pos"]     { --accent:#1d4ed8; --accent-ink:#1e40af; --accent-soft:#e0e9fd; --accent-wash:#f6f8fe; }
body[data-page="inventory"]        { --accent:#15803d; --accent-ink:#166534; --accent-soft:#dcf4e3; --accent-wash:#f5fbf7; }
body[data-page="inventory-items"]  { --accent:#15803d; --accent-ink:#166534; --accent-soft:#dcf4e3; --accent-wash:#f5fbf7; }
body[data-page="inventory-inverters"]{ --accent:#047857; --accent-ink:#065f46; --accent-soft:#d6f2e7; --accent-wash:#f4fbf8; }
body[data-page="inventory-sales"]  { --accent:#a21caf; --accent-ink:#86198f; --accent-soft:#fae3fb; --accent-wash:#fdf6fd; }
body[data-page="stock-report"]     { --accent:#0e7490; --accent-ink:#155e75; --accent-soft:#d7f0f6; --accent-wash:#f4fbfc; }
body[data-page="expenses"]         { --accent:#0891b2; --accent-ink:#0e7490; --accent-soft:#d8f1f8; --accent-wash:#f4fbfd; }
body[data-page="fund-transfers"]   { --accent:#7c3aed; --accent-ink:#6d28d9; --accent-soft:#eae4fe; --accent-wash:#f8f6fe; }
body[data-page="statement"]        { --accent:#4338ca; --accent-ink:#3730a3; --accent-soft:#e6e6fc; --accent-wash:#f7f7fe; }
body[data-page="statements"]       { --accent:#4338ca; --accent-ink:#3730a3; --accent-soft:#e6e6fc; --accent-wash:#f7f7fe; }
body[data-page="tickets"]          { --accent:#dc2626; --accent-ink:#b91c1c; --accent-soft:#fde2e2; --accent-wash:#fdf6f6; }
body[data-page="staff"]            { --accent:#9333ea; --accent-ink:#7e22ce; --accent-soft:#f0e3fd; --accent-wash:#fbf7fe; }
body[data-page="staff-payments"]   { --accent:#0d9488; --accent-ink:#0f766e; --accent-soft:#d7f3ef; --accent-wash:#f4fbfa; }
body[data-page="staff-activity"]   { --accent:#9333ea; --accent-ink:#7e22ce; --accent-soft:#f0e3fd; --accent-wash:#fbf7fe; }
/* Cash position — deep emerald. Reads as "the money itself", and sits apart
   from the teal of staff payments, which is only one outflow within it. */
body[data-page="treasury"]         { --accent:#047857; --accent-ink:#065f46; --accent-soft:#d3f0e3; --accent-wash:#f4fbf8; }
/* Requests — warm clay. The one money page every member of staff opens, so it
   is deliberately the friendliest of the set rather than another finance green. */
body[data-page="requests"]         { --accent:#c2410c; --accent-ink:#9a3412; --accent-soft:#fbe3d5; --accent-wash:#fdf8f5; }
/* Website content — brand blue. This page edits what the public sees, so it
   wears the public site's own colour rather than an internal working hue. */
body[data-page="site-content"]     { --accent:#2f52a0; --accent-ink:#1e3a72; --accent-soft:#e8eefb; --accent-wash:#f6f8fd; }
body[data-page="settings"]         { --accent:#525252; --accent-ink:#404040; --accent-soft:#eceae8; --accent-wash:#faf9f8; }

/* The page's own header carries its colour: a warm wash and a hairline of
   the accent along the top, enough to place you without shouting. */
body[data-page] main > header {
  background: var(--accent-wash) !important;
  box-shadow: inset 0 3px 0 var(--accent);
}
body[data-page] main > header h1 { color: var(--accent-ink); }

/* Warm the page behind the content, off the cold blue-grey default */
body[data-page].bg-slate-50,
body[data-page].bg-slate-100 { background-color: #faf9f7; }

/* Wayfinding in the sidebar picks up the area colour too */
body[data-page] .sidebar-link.active { box-shadow: inset 3px 0 0 var(--accent); }

/* Filter chips follow the area rather than all being brand blue */
body[data-page] .chip.active {
  background: var(--accent) !important;
  border-color: var(--accent) !important;
}

/* ── Content placeholders ─────────────────────────────────────────────────
   Slots waiting on real content (site photos, case-study figures, licence
   numbers).

   Visitors must never see these: an editorial marker or a literal
   "[Licence no.]" on a live page looks unfinished and gives away what we do
   not yet have. So unfilled content is hidden outright, and only staff with
   a session see it, flagged in amber so it still cannot be forgotten.

   Hidden by default rather than shown-then-hidden, so a visitor never
   catches a flash of it before script runs. Search for `data-placeholder`
   to find them; delete the attribute and the class once content is in.    */
html:not(.staff-preview) [data-placeholder] { display: none !important; }

/* The amber flagging is a staff-only editing aid. Some marked elements are
   perfectly presentable already (the partner tiles show real brand names and
   are only waiting on a logo image), so the marker must never be what a
   visitor sees. Gated on the same class as the hiding rule above. */
html.staff-preview .placeholder {
  position: relative;
  border: 2px dashed #f4991f !important;
  background: rgba(244, 153, 31, 0.05);
}
html.staff-preview .placeholder::after {
  content: "NEEDS CONTENT";
  position: absolute;
  top: 0; right: 0;
  background: #f4991f;
  color: #fff;
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 0.06em;
  padding: 2px 6px;
  border-bottom-left-radius: 0.375rem;
}

/* On a short line of text the corner tag lands on top of the words it is
   meant to flag. There the marker leads the text instead of overlaying it. */
html.staff-preview .placeholder-inline::after { content: none; }
html.staff-preview .placeholder-inline::before {
  content: "NEEDS CONTENT · ";
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 0.06em;
  color: #f4991f;
}

/* ── Loading skeletons ────────────────────────────────────────────────────
   Shown while catalog/projects data is in flight, so a page never flashes
   an empty state that turns out to be wrong.                               */
.skeleton {
  background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 37%, #f1f5f9 63%);
  background-size: 400% 100%;
  animation: skeleton-sweep 1.4s ease-in-out infinite;
  border-radius: 0.5rem;
}
@keyframes skeleton-sweep {
  0%   { background-position: 100% 50%; }
  100% { background-position: 0 50%; }
}

/* ── Motion preferences ───────────────────────────────────────────────────
   Reduced motion means less movement, not no animation. Travel, scaling and
   looping are removed; the opacity fades stay, because they are what tells
   you a dialog opened rather than the page silently rearranging itself. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto !important; }

  /* Dialogs and the nav still fade in, minus the scale and slide */
  .fixed.inset-0.z-50.items-center.justify-center:not(.hidden) > *,
  #mobile-menu:not(.hidden) {
    animation-name: fade-in-only;
  }

  /* Position and scale changes off */
  button:not(:disabled):active,
  a[class*="rounded"]:active { transform: none !important; }
  .card-hover:hover { transform: none !important; }

  /* Looping motion is exactly what this setting is for. The breakdown line
     keeps its solid dot, just without the pulse. */
  .skeleton { animation: none; background: #eef1f5; }
  [style*="ping-slow"] { animation: none !important; opacity: 0 !important; }
}
@keyframes fade-in-only {
  from { opacity: 0; }
}

/* ── Sidebar scrolling ────────────────────────────────────────────────────
   The sidebar markup is copied into every page, and only five of the
   twenty-four pages ever got `overflow-y-auto` on the nav. On the other
   nineteen the link list simply overflowed the panel with no way to reach
   the bottom of it. Setting it here fixes all of them at once, and means a
   page copied from one of the nineteen in future inherits the fix rather
   than the bug.

   min-height:0 goes with it: a flex item defaults to min-height:auto, which
   refuses to shrink below its own content and quietly cancels the scrolling
   it is paired with. */
#sidebar > nav {
  overflow-y: auto;
  min-height: 0;
}

/* ── Mobile sidebar drawer ────────────────────────────────────────────────
   Below lg the sidebar is a fixed off-canvas drawer, where inset-y-0 is
   measured against the LARGE viewport: the one you get once the browser's
   URL bar has retracted. While that bar is showing, the drawer's bottom edge
   sits below the visible screen, so even a scrolling nav cannot bring the
   last links into view. 100dvh tracks the viewport as the bar comes and goes.

   Scroll chaining is contained as well, so flicking the menu past its end
   does not start scrolling the page behind it. */
@media (max-width: 1023px) {
  #sidebar {
    height: 100vh;            /* for browsers without dvh */
    height: 100dvh;
  }
  #sidebar > nav {
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    padding-bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px));
  }
  /* The account chip is the last thing in the drawer, so keep it clear of the
     home indicator on phones that have one. */
  #sidebar > :last-child {
    padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
  }
  /* While the drawer is open the page behind it must stay put: otherwise a
     drag that begins on the drawer can scroll the page instead of the menu. */
  html.sidebar-open, html.sidebar-open body { overflow: hidden; }
}
