/* ==========================================================
   Triple H Client Portal — app shell
   ==========================================================
   Added 2026-09-04, requested directly: "Make boxes pop. Make
   swiping feel good, but don't over do it just clean everything up
   and make it feel more user friendly and app facing."

   Three things live here, all previously either missing or copy-
   pasted into every page's own inline <style>:

   1. BOTTOM TAB BAR. The single biggest structural change. The nav
      used to be five text links in a grid at the TOP of the page
      that scrolled away with the content -- a website pattern. Every
      native convention puts 3-5 primary destinations in a FIXED bar
      at the bottom, because the bottom third of the screen is the
      most reachable area for one-handed thumb use. Five destinations
      is the documented ceiling (more than five puts targets close
      enough together that people trigger the wrong one), and the
      portal has exactly five, so this fits without cutting anything.
      Settings deliberately stays OUT of the bar and remains a header
      icon button, matching the convention that a tab bar holds
      navigation destinations, not actions or account screens.

   2. CARD ELEVATION. Cards were a flat 1px border on a near-black
      background, which reads as a document rather than an interface.
      A subtle raised surface plus a real shadow gives depth without
      tipping into skeuomorphism.

   3. TAP FEEDBACK. Buttons had hover states, which do nothing on a
      phone. Without a visible response to a tap, people tap twice
      and fire the wrong action. Every interactive surface now has a
      real :active state.

   Deliberately NOT done here: heavy animation. The request was
   explicitly "don't over do it." Transitions are short (120-180ms)
   and limited to transform/opacity/background, which the compositor
   handles without repainting.
   ========================================================== */

/* ----------------------------------------------------------
   1. Bottom tab bar
   ---------------------------------------------------------- */

.portal-nav {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 90;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 0;
  /* The iPhone home indicator sits inside the viewport -- without
     this the last row of the bar is physically under it. max() keeps
     a real floor on browsers where env() resolves to 0px, the same
     guard the internal tools app already needed after a real
     reported bug. */
  padding: 6px 4px max(6px, env(safe-area-inset-bottom));
  background: rgba(14, 14, 14, 0.92);
  border-top: 1px solid var(--border);
  /* Content scrolling under a translucent bar reads as glass rather
     than as a gap; the solid fallback above still applies wherever
     backdrop-filter is unsupported. */
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}

.portal-nav a {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  /* 44px is the documented minimum comfortable touch target. */
  min-height: 44px;
  padding: 4px 2px;
  text-decoration: none;
  color: var(--text-dim);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.2px;
  line-height: 1.2;
  border: 0;
  border-radius: 10px;
  background: none;
  transition: color .15s ease, transform .12s ease;
  -webkit-tap-highlight-color: transparent;
}

.portal-nav a svg {
  width: 22px;
  height: 22px;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform .15s ease;
}

.portal-nav a.is-active {
  color: var(--orange-light);
  /* No filled pill behind the icon -- the colour change plus the dot
     below is enough to read at a glance without adding visual weight
     to a bar that's on screen at all times. */
  background: none;
  border: 0;
}

.portal-nav a.is-active svg {
  transform: translateY(-1px);
}

/* Small active indicator, the pattern iOS and Android both settled
   on for showing the current destination without a heavy highlight. */
.portal-nav a.is-active::after {
  content: '';
  position: absolute;
  bottom: 2px;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--orange-light);
}

.portal-nav a {
  position: relative;
}

.portal-nav a:active {
  transform: scale(0.94);
}

/* The bar is fixed, so it no longer occupies layout space -- without
   this the last card on every page sits underneath it and can't be
   scrolled to. */
body.portal-page {
  padding-bottom: calc(72px + env(safe-area-inset-bottom));
}

/* A full-width bar looks stranded on a desktop window, where the
   content column is centred and narrow. Matching the container's own
   width keeps it visually attached to the content it belongs to. */
@media (min-width: 720px) {
  .portal-nav {
    left: 50%;
    transform: translateX(-50%);
    right: auto;
    width: min(560px, calc(100% - 32px));
    margin-bottom: 14px;
    border: 1px solid var(--border);
    border-radius: 16px;
    box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
  }
}

/* ----------------------------------------------------------
   2. Card elevation
   ---------------------------------------------------------- */

.attention-card,
.help-card,
.home-card,
.invoice-card,
.job-card,
.quote-card,
.set-card,
.wo-card,
.wo-form-card {
  /* A hair lighter than the page behind it -- the surface itself
     does most of the separating; the shadow only anchors it. */
  background: var(--bg-panel);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 4px 14px rgba(0, 0, 0, 0.22);
}

/* .attention-card keeps its own orange tint from the page it lives
   on -- overriding the background here would flatten the one card
   that is deliberately supposed to stand out from the rest. */
.attention-card {
  background: var(--orange-tint-soft);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 4px 16px rgba(245, 129, 31, 0.10);
}

/* ----------------------------------------------------------
   3. Tap feedback
   ---------------------------------------------------------- */

.btn,
.small-btn,
.secondary-btn,
.portal-icon-btn {
  transition: transform .12s ease, filter .12s ease, background .15s ease;
  -webkit-tap-highlight-color: transparent;
}

.btn:active,
.small-btn:active,
.secondary-btn:active,
.portal-icon-btn:active {
  transform: scale(0.97);
  filter: brightness(0.92);
}

/* A card that actually navigates somewhere should say so on touch;
   one that's only a container should not. Applied via a class rather
   than to every card, so it stays opt-in per page. */
.is-tappable {
  cursor: pointer;
  transition: transform .12s ease, box-shadow .12s ease;
  -webkit-tap-highlight-color: transparent;
}

.is-tappable:active {
  transform: scale(0.985);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
}

/* ----------------------------------------------------------
   4. Skeleton loading states
   ----------------------------------------------------------
   Added 2026-09-04, requested directly: "Skeleton loading states."
   Grounded in the research: skeletons for content being fetched
   where layout context matters (lists, dashboards) -- spinners stay
   for short blocking actions (payment, auth) where content structure
   is unknown, which is why the payment modal and set-password's
   token check deliberately still show plain text, not a skeleton
   here. A generic card shape (title bar + two lines) is used
   everywhere rather than a bespoke skeleton per page -- it's a
   reasonable approximation of every real card class on the portal,
   and building one per page would be much more work for a
   perceptual improvement that doesn't need pixel-perfect matching to
   work. */

@keyframes skeleton-pulse {
  0%, 100% { opacity: 0.55; }
  50% { opacity: 0.9; }
}

.skeleton-card {
  background: var(--bg-panel);
  border-radius: 12px;
  padding: 16px;
  margin-bottom: 12px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 4px 14px rgba(0, 0, 0, 0.22);
}

.skeleton-line {
  height: 12px;
  border-radius: 6px;
  background: var(--border);
  animation: skeleton-pulse 1.3s ease-in-out infinite;
}

.skeleton-line + .skeleton-line {
  margin-top: 10px;
}

.skeleton-line.is-title { width: 55%; height: 15px; }
.skeleton-line.is-wide { width: 90%; }
.skeleton-line.is-medium { width: 70%; }
.skeleton-line.is-narrow { width: 40%; }

/* A smaller variant for nested contexts (a message panel inside an
   already-open card), where a full card-shaped skeleton would be
   visually too heavy for the space it sits in. */
.skeleton-mini {
  padding: 4px 0;
}

.skeleton-mini .skeleton-line {
  height: 10px;
}

@media (prefers-reduced-motion: reduce) {
  .skeleton-line {
    animation: none;
    opacity: 0.7;
  }
}

   ----------------------------------------------------------
   Vestibular-motion sensitivity is a real accessibility need, and
   every transition above is decorative rather than load-bearing --
   removing them costs nothing functionally. */
@media (prefers-reduced-motion: reduce) {
  .portal-nav a,
  .portal-nav a svg,
  .btn,
  .small-btn,
  .secondary-btn,
  .portal-icon-btn,
  .is-tappable {
    transition: none;
  }

  .portal-nav a:active,
  .btn:active,
  .small-btn:active,
  .secondary-btn:active,
  .portal-icon-btn:active,
  .is-tappable:active {
    transform: none;
  }
}

/* ----------------------------------------------------------
   5. Offline indicator
   ----------------------------------------------------------
   Added 2026-09-04, requested directly: "offline indicator." The
   service worker's own app-shell caching means pages OPEN fine with
   no connection, but nothing told a client they were looking at
   possibly-stale data or that a failed action might just be the
   network, not a real bug -- a failed save currently just looks
   broken. A small, unobtrusive banner rather than a full-screen
   takeover: this is informational, not blocking, and a client should
   still be able to read what's already loaded. */

.offline-banner {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 95;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: calc(8px + env(safe-area-inset-top)) 12px 8px;
  background: #3a2a10;
  color: #ffb84d;
  font-size: 12.5px;
  font-weight: 600;
  text-align: center;
  transform: translateY(-100%);
  transition: transform .18s ease;
}

.offline-banner.is-visible {
  transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  .offline-banner {
    transition: none;
  }
}

/* ----------------------------------------------------------
   6. Pull-to-refresh
   ----------------------------------------------------------
   Added 2026-09-04, requested directly, with an explicit constraint
   in mind: "Make swiping feel good, but don't over do it." A single,
   restrained gesture -- pull down from the very top of a list, see a
   small spinner grow in, release past a threshold to refresh --
   rather than layering in a whole gesture library. */

.ptr-indicator {
  position: fixed;
  top: 0;
  left: 50%;
  transform: translate(-50%, -100%);
  z-index: 85;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
  display: flex;
  align-items: center;
  justify-content: center;
  /* No CSS transition on transform here -- while dragging, position
     must track the finger exactly with zero lag. The snap-back and
     the spin animation below are the only animated parts. */
}

.ptr-indicator svg {
  width: 18px;
  height: 18px;
  stroke: var(--orange-light);
  fill: none;
  stroke-width: 2.2;
  stroke-linecap: round;
}

.ptr-indicator.is-snapping {
  transition: transform .18s ease;
}

.ptr-indicator.is-refreshing svg {
  animation: ptr-spin .7s linear infinite;
}

@keyframes ptr-spin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .ptr-indicator.is-refreshing svg {
    animation: none;
  }
}

/* ----------------------------------------------------------
   Biometric app lock
   ----------------------------------------------------------
   Added 2026-09-05, requested directly: "biometric unlock." A
   full-screen overlay, shown only while a client-chosen local lock
   is being verified -- see portal-app.js's own header comment on
   this feature for the full reasoning (a local-only gate in front
   of an already-valid session, not a server-verified factor).
*/

.biometric-lock-overlay {
  position: fixed;
  inset: 0;
  z-index: 999;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: var(--bg);
}

.biometric-lock-box {
  width: 100%;
  max-width: 320px;
  text-align: center;
}

.biometric-lock-icon {
  width: 48px;
  height: 48px;
  margin: 0 auto 18px;
  display: block;
  stroke: var(--orange-light);
  fill: none;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.biometric-lock-title {
  font-family:var(--font-ui);
  font-weight: 700;
  font-size: 20px;
  color: var(--white);
  margin-bottom: 8px;
}

.biometric-lock-sub {
  color: var(--text-dim);
  font-size: 13.5px;
  line-height: 1.5;
}

/* Themed replacement for window.confirm() (2026-09-07) -- same visual
   language as the app's other modals (dashboard.html's own payment
   modal and every page's report-a-problem modal): dark scrim,
   bg-panel card, 12px radius. Built once here since portal-app.js
   injects this markup lazily on first use rather than every page
   carrying its own static copy. z-index sits above both of those
   (9999/1000) since it can, in principle, be invoked from a page that
   also has one of them open. */
.portal-confirm-overlay {
  position: fixed;
  inset: 0;
  z-index: 10000;
  background: rgba(0,0,0,0.65);
  display: none;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.portal-confirm-overlay.is-visible { display: flex; }
.portal-confirm-modal {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 22px;
  max-width: 380px;
  width: 100%;
}
.portal-confirm-message {
  margin: 0 0 18px;
  color: var(--text);
  font-size: 14.5px;
  line-height: 1.5;
}
.portal-confirm-actions {
  display: flex;
  gap: 10px;
}
.portal-confirm-actions button {
  flex: 1;
  justify-content: center;
}
