/* ===========================================================================
   timepro.css  —  THE global stylesheet for TimeProHR
   ---------------------------------------------------------------------------
   Built on the RealtyFin chassis, consolidated into a single file per spec:
   one CSS, one JS, no <style> block and no style="" in any .aspx.

   If a screen needs something this file cannot do, the fix is to add a class
   HERE so every screen gets it — never a one-off in the form.

   SECTIONS
     1  Tokens                 9  Field chassis (12-col)
     2  Reset                 10  Data grids
     3  App shell             11  KPI tiles
     4  Topbar                12  Toast
     5  Sidebar menu          13  Modal
     6  Tab shell             14  Auth / state screens
     7  Page head + card      15  Report
     8  Buttons + badges      16  Utilities
                              17  Responsive
                              18  RTL
                              19  Print
   =========================================================================== */


/* ===========================================================================
   FONTS
   Roboto for Latin, Cairo for Arabic, both served from Assets/fonts.

   THERE IS NO @import. It used to fetch from fonts.googleapis.com, which
   meant a blocking request to an outside service on every cold load — and
   on a customer network that cannot reach Google, a silent fall back to
   Segoe UI that nobody could diagnose. The files are local now, so the
   import was removed rather than left as an unused fallback.

   IIS must be told to serve .woff2. It has no MIME type for unmapped
   extensions and answers 404, which looks exactly like the font not
   existing — see the staticContent section in web.config.
   =========================================================================== */

@font-face{
  font-family:"Roboto";font-style:normal;font-weight:400;font-display:swap;
  src:url("../fonts/Roboto-Regular.woff2") format("woff2");
}
@font-face{
  font-family:"Roboto";font-style:normal;font-weight:500;font-display:swap;
  src:url("../fonts/Roboto-Medium.woff2") format("woff2");
}
@font-face{
  font-family:"Roboto";font-style:normal;font-weight:700;font-display:swap;
  src:url("../fonts/Roboto-Bold.woff2") format("woff2");
}
@font-face{
  font-family:"Cairo";font-style:normal;font-weight:400;font-display:swap;
  src:url("../fonts/Cairo-Regular.woff2") format("woff2");
}
@font-face{
  font-family:"Cairo";font-style:normal;font-weight:600;font-display:swap;
  src:url("../fonts/Cairo-SemiBold.woff2") format("woff2");
}
@font-face{
  font-family:"Cairo";font-style:normal;font-weight:700;font-display:swap;
  src:url("../fonts/Cairo-Bold.woff2") format("woff2");
}

/* ---------- 1. TOKENS ---------------------------------------------------- */
:root{
  /* surface — white per spec; depth comes from hairlines, not grey fills */
  --tp-bg:#ffffff;
  --tp-surface:#ffffff;
  --tp-surface-2:#fbfcfe;        /* disabled input, quiet fills */
  --tp-headbg:#f4f6fa;           /* card header AND grid header — one value */
  --tp-line:#e4e9f0;
  --tp-line-strong:#cfd8e3;

  /* ink */
  --tp-ink:#1b2430;
  --tp-ink-2:#475467;
  --tp-muted:#7a8699;
  --tp-ink-invert:#ffffff;

  /* brand — deep harbour blue. Change these three lines to rebrand. */
  --tp-primary:#3f3ba6;
  --tp-primary-dark:#312d8a;
  --tp-primary-soft:#eef0fa;

  /* semantic */
  --tp-ok:#0f7b5f;      --tp-ok-soft:#e8f5f0;
  --tp-warn:#b26a00;    --tp-warn-soft:#fdf3e3;
  --tp-danger:#b3261e;  --tp-danger-soft:#fdecea;
  --tp-info:#3f3ba6;    --tp-info-soft:#eef0fa;

  /* attendance semantics — present / absent / late read the same everywhere */
  --tp-present:#0f7b5f;
  --tp-absent:#b3261e;
  --tp-late:#b26a00;

  /* geometry */
  --tp-r-sm:4px;  --tp-r:6px;  --tp-r-lg:10px;
  --tp-gap:12px;  --tp-gap-lg:18px;
  --tp-touch:44px;                /* minimum tap target, never reduced */
  --tp-field-h:38px;
  --tp-topbar-h:56px;
  --tp-tabbar-h:42px;
  --tp-side-w:250px;
  --tp-side-w-rail:62px; /* collapsed: one icon plus breathing room */
  --tp-cardhead-h:46px;     /* every card header, list and form alike */
  --tp-act-w:38px;      /* one action cell, used to offset pinned columns */
  --tp-row-h:32px;          /* grid body row */
  --tp-rowhead-h:34px;      /* grid header row */
  --tp-footbar-h:46px;      /* command bar AND sidebar foot */
  --tp-appfoot-h:28px;

  /* type — system stack: an on-prem product cannot depend on a font CDN */
  --tp-font:"Roboto","Segoe UI",-apple-system,BlinkMacSystemFont,"Helvetica Neue",
           Arial,sans-serif;
  --tp-font-ar:"Cairo","Roboto","Segoe UI","Tahoma",sans-serif;
  --tp-fs:14px; --tp-fs-sm:13px; --tp-fs-lg:16px;
  --tp-lh:1.5;

  --tp-shadow:0 1px 2px rgba(16,24,40,.06),0 2px 8px -2px rgba(16,24,40,.08);
  --tp-shadow-lg:0 8px 28px rgba(16,24,40,.14);
  --tp-focus:0 0 0 3px rgba(63,59,166,.24);
  --tp-t:.16s ease;

  /* legacy aliases so older markup keeps resolving */
  --tp-canvas:var(--tp-bg);
  --tp-border:var(--tp-line);
  --tp-border-strong:var(--tp-line-strong);
  --tp-text:var(--tp-ink);
  --tp-text-muted:var(--tp-muted);
  --tp-success:var(--tp-ok);
  --tp-warning:var(--tp-warn);
  --tp-radius:var(--tp-r-lg);
  --tp-radius-sm:var(--tp-r);
  --tp-ctl-h:var(--tp-field-h);
}

/* ---------- 2. RESET ----------------------------------------------------- */
*,*::before,*::after{box-sizing:border-box}
html,body{height:100%}
/* Only a FRAMED page. The login and change-password pages are standalone
   and must keep normal document scrolling, or on a short screen the form
   becomes unreachable. */
body.tp-framed{position:relative;overflow:hidden}
body{
  margin:0;background:var(--tp-bg);color:var(--tp-ink);
  font:400 var(--tp-fs)/var(--tp-lh) var(--tp-font);
  -webkit-text-size-adjust:100%;
  -webkit-tap-highlight-color:transparent;
  /* Without these the same weight reads noticeably heavier on Windows than
     on a Mac, and the interface looks muddy at 13-14px. */
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
  text-rendering:optimizeLegibility;
}
h1,h2,h3,h4,h5,h6,p,figure{margin:0}
img,svg{max-width:100%;height:auto;vertical-align:middle}
a{color:var(--tp-primary);text-decoration:none}
a:hover{text-decoration:underline}
:focus{outline:none}
:focus-visible{outline:none;box-shadow:var(--tp-focus);border-radius:var(--tp-r-sm)}

/* numerals line up in every count and time column, without a class */
input,table,.tp-num{font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.01ms!important;transition-duration:.01ms!important}
}

/* ---------- 3. APP SHELL ------------------------------------------------- */
/* Shell.aspx wraps everything in a server form: that form must not break the
   full-height flex chain between <body> and .tp-shell. */
.tp-shell-host{height:100%;margin:0}

/* Column: the sidebar+workspace row on top, the application footer beneath
   both. The footer runs the full width of the window, under the sidebar too.

   .tp-shell carries its own full height as well as the flex rule, so the
   layout does not silently collapse if the .tp-app wrapper is absent. A
   stylesheet that only works with one exact tree is a trap. */
.tp-app{display:flex;flex-direction:column;height:100vh;height:100dvh;overflow:hidden}
.tp-shell{
  display:flex;overflow:hidden;background:var(--tp-bg);
  height:100vh;height:100dvh;min-height:0;
}
.tp-app>.tp-shell{flex:1 1 auto;height:auto}

.tp-appfoot{
  flex:0 0 auto;height:var(--tp-appfoot-h);
  display:flex;align-items:center;justify-content:space-between;gap:12px;
  padding:0 16px;
  background:var(--tp-surface);border-top:1px solid var(--tp-line);
  font-size:12.5px;color:var(--tp-muted);font-weight:400;
}
.tp-appfoot-start{color:var(--tp-muted);font-weight:400}
.tp-appfoot-end{white-space:nowrap}
.tp-main{flex:1 1 auto;display:flex;flex-direction:column;min-width:0}
.tp-workspace{flex:1 1 auto;position:relative;overflow:hidden;background:var(--tp-bg)}

/* Each open tab is one iframe. Inactive tabs are hidden, never destroyed, so
   a half-filled form survives while the user checks something elsewhere. */
.tp-frame{position:absolute;inset:0;width:100%;height:100%;border:0;background:var(--tp-bg)}
.tp-frame.tp-inactive{visibility:hidden;z-index:0}
.tp-frame.tp-active{visibility:visible;z-index:1}

/* ---------- 4. TOPBAR ---------------------------------------------------- */
.tp-topbar{
  height:var(--tp-topbar-h);flex:0 0 auto;
  display:flex;align-items:center;gap:10px;padding:0 14px;
  background:var(--tp-surface);border-bottom:1px solid var(--tp-line);z-index:30;
}
.tp-brand{display:flex;align-items:center;gap:10px;min-width:0}
.tp-brand-mark{
  width:36px;height:36px;flex:0 0 36px;border-radius:10px;
  display:grid;place-items:center;
  background:var(--tp-primary-soft);color:var(--tp-primary);
}
.tp-brand-text{display:flex;flex-direction:column;line-height:1.2;min-width:0}
.tp-brand-name{font-weight:600;font-size:15px;color:var(--tp-ink)}
.tp-brand-sub{font-size:11.5px;color:var(--tp-muted)}

.tp-burger{
  display:none;min-width:var(--tp-touch);min-height:var(--tp-touch);
  border:0;background:transparent;border-radius:var(--tp-r);
  cursor:pointer;align-items:center;justify-content:center;color:var(--tp-ink-2);
}
.tp-burger:hover{background:var(--tp-primary-soft);color:var(--tp-primary)}
.tp-topbar-spacer{flex:1 1 auto}

.tp-topbar-item{display:flex;flex-direction:column;align-items:flex-end;line-height:1.25}
.tp-topbar-item .tp-cap{
  font-size:10.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--tp-muted);
}
.tp-topbar-item strong{font-size:13.5px;font-weight:600;color:var(--tp-ink)}

.tp-userbox{display:flex;align-items:center;gap:9px}
.tp-userbox .tp-uname{font-size:13.5px;font-weight:600;line-height:1.2}
.tp-userbox .tp-urole{font-size:11.5px;color:var(--tp-muted);line-height:1.2}
.tp-avatar{
  width:34px;height:34px;flex:0 0 34px;border-radius:50%;
  display:grid;place-items:center;
  background:var(--tp-primary);color:var(--tp-ink-invert);font-weight:600;font-size:13px;
}

.tp-iconbtn{
  display:inline-flex;align-items:center;justify-content:center;
  min-width:var(--tp-touch);min-height:var(--tp-touch);
  border:0;background:transparent;color:var(--tp-ink-2);
  border-radius:var(--tp-r);cursor:pointer;font-size:12.5px;font-weight:600;
  transition:background var(--tp-t),color var(--tp-t);
}
.tp-iconbtn:hover{background:var(--tp-primary-soft);color:var(--tp-primary);text-decoration:none}

.tp-signout{
  display:inline-flex;align-items:center;gap:7px;
  min-height:38px;padding:0 13px;
  border:1px solid var(--tp-line-strong);border-radius:var(--tp-r);
  color:var(--tp-ink);background:var(--tp-surface);
  font-size:13px;font-weight:600;text-decoration:none;
  transition:background var(--tp-t),border-color var(--tp-t),color var(--tp-t);
}
.tp-signout:hover{
  background:var(--tp-danger-soft);color:var(--tp-danger);
  border-color:rgba(179,38,30,.35);text-decoration:none;
}

/* ---------- 5. SIDEBAR MENU ---------------------------------------------- */
.tp-sidebar{
  flex:0 0 var(--tp-side-w);width:var(--tp-side-w);
  background:var(--tp-surface);color:var(--tp-ink-2);
  border-inline-end:1px solid var(--tp-line);
  display:flex;flex-direction:column;overflow:hidden;z-index:40;
  transition:transform var(--tp-t);
}
/* Logo strip aligns with the topbar across the vertical split. */
.tp-sidebar-head{
  height:var(--tp-topbar-h);flex:0 0 auto;
  display:flex;align-items:center;padding:0 16px;
  border-bottom:1px solid var(--tp-line);background:var(--tp-surface);
  justify-content:flex-start;
}
.tp-sidebar-logo-link{display:flex;align-items:center;gap:10px;min-width:0;text-decoration:none;color:var(--tp-ink)}
.tp-sidebar-logo-link:hover{text-decoration:none;opacity:.85}
.tp-sidebar-logo{
  max-height:36px;max-width:calc(var(--tp-side-w) - 32px);
  width:auto;height:auto;display:block;object-fit:contain;
}

.tp-sidebar-body{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:8px 0 24px}
.tp-sidebar-body::-webkit-scrollbar{width:6px}
.tp-sidebar-body::-webkit-scrollbar-thumb{background:var(--tp-line-strong);border-radius:3px}

.tp-menu-link{
  display:flex;align-items:center;gap:11px;
  min-height:var(--tp-touch);padding:0 16px;
  color:var(--tp-ink-2);font-size:13.5px;
  border-inline-start:3px solid transparent;
  cursor:pointer;user-select:none;text-decoration:none;
  transition:background var(--tp-t),color var(--tp-t);
}
.tp-menu-link:hover{background:var(--tp-primary-soft);color:var(--tp-primary);text-decoration:none}
.tp-menu-link.tp-current{
  color:var(--tp-primary);font-weight:600;
  background:var(--tp-primary-soft);border-inline-start-color:var(--tp-primary);
}
.tp-menu-ico{width:20px;height:20px;flex:0 0 20px;display:grid;place-items:center}
.tp-menu-ico svg{width:19px;height:19px;display:block}

.tp-menu-caret{margin-inline-start:auto;display:grid;place-items:center;opacity:.45;transition:transform var(--tp-t)}
.tp-menu-caret svg{width:14px;height:14px}
.tp-menu-group.tp-open>.tp-menu-link .tp-menu-caret{transform:rotate(90deg)}
.tp-menu-group.tp-open>.tp-menu-link{color:var(--tp-ink)}

.tp-submenu{display:none}
.tp-menu-group.tp-open>.tp-submenu{display:block}
.tp-submenu .tp-menu-link{
  padding-inline-start:44px;font-size:13px;min-height:38px;position:relative;
}
.tp-submenu .tp-menu-link::before{
  content:"";position:absolute;inset-inline-start:24px;top:50%;
  width:5px;height:5px;margin-top:-2.5px;border-radius:50%;
  background:currentColor;opacity:.35;
}
.tp-submenu .tp-menu-link.tp-current::before{opacity:1}

.tp-sidebar-foot{
  flex:0 0 auto;height:var(--tp-footbar-h);
  display:flex;align-items:center;
  padding:0 16px;border-top:1px solid var(--tp-line);
  font-size:12px;color:var(--tp-muted);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.tp-backdrop{
  display:none;position:fixed;inset:0;z-index:35;background:rgba(16,24,40,.42);
}
.tp-backdrop.tp-show{display:block}

/* ---------- 6. TAB SHELL ------------------------------------------------- */
.tp-tabbar{
  height:var(--tp-tabbar-h);flex:0 0 auto;
  display:flex;align-items:stretch;
  background:var(--tp-surface);border-bottom:1px solid var(--tp-line);
  box-shadow:0 1px 3px rgba(16,24,40,.05);z-index:20;
}

/* scroll arrows: shown only when the strip actually overflows */
.tp-tabscroll{
  flex:0 0 auto;width:30px;
  display:none;align-items:center;justify-content:center;
  border:0;background:transparent;color:var(--tp-muted);cursor:pointer;
}
.tp-tabscroll.tp-show{display:inline-flex}
.tp-tabscroll:hover{background:var(--tp-surface-2);color:var(--tp-primary)}
.tp-tabscroll svg{width:16px;height:16px}
body[dir="rtl"] .tp-tabscroll svg{transform:scaleX(-1)}

/* Source tabs on the punch list: Direct, Schedule, File. Named
   tp-punchtabs rather than tp-tabs, because tp-tabscroll and
   tp-tabs-scroll already exist and a third near-identical name is how
   somebody edits the wrong rule. */
/* A card holding nothing but tabs: no body padding, so the tab row sits
   flush and does not read as an empty card with something missing. */
.tp-card-flush{padding:0;overflow:hidden}
.tp-card-flush .tp-punchtabs{border-bottom:0}

.tp-punchtabs{display:flex;gap:4px;padding:0 14px;border-bottom:1px solid var(--tp-line)}
.tp-punchtab{
  padding:9px 14px;border:0;background:none;cursor:pointer;
  color:var(--tp-muted);font-weight:600;font-size:13px;
  border-bottom:2px solid transparent;text-decoration:none
}
.tp-punchtab:hover{color:var(--tp-text)}
.tp-punchtab-on{color:var(--tp-primary);border-bottom-color:var(--tp-primary)}
.tp-icon-btn{font-size:12px;font-weight:600;text-decoration:none;padding:2px 6px}

.tp-tabs-scroll{
  flex:1 1 auto;min-width:0;display:flex;align-items:stretch;gap:0;
  overflow-x:auto;overflow-y:hidden;
  scrollbar-width:none;-webkit-overflow-scrolling:touch;
}
.tp-tabs-scroll::-webkit-scrollbar{display:none}

.tp-tab{
  flex:0 0 auto;position:relative;
  display:inline-flex;align-items:center;gap:9px;
  max-width:240px;padding:0 13px 0 17px;
  font-family:var(--tp-font);font-size:14px;font-weight:500;
  letter-spacing:-.005em;color:var(--tp-ink-2);
  background:transparent;border:0;
  border-bottom:2px solid transparent;
  cursor:pointer;white-space:nowrap;
  transition:background var(--tp-t),color var(--tp-t);
}
.tp-tab:hover{background:var(--tp-surface-2);color:var(--tp-ink)}
.tp-tab.tp-active{
  color:var(--tp-primary);font-weight:600;
  border-bottom-color:var(--tp-primary);background:transparent;
}
.tp-tab-label{overflow:hidden;text-overflow:ellipsis}

/* the pinned home tab carries a dot instead of a close cross */
.tp-tab-dot{
  width:6px;height:6px;flex:0 0 6px;border-radius:50%;
  background:var(--tp-line-strong);
}
.tp-tab.tp-active .tp-tab-dot{background:var(--tp-primary)}

.tp-tab-x{
  display:inline-flex;align-items:center;justify-content:center;
  width:20px;height:20px;flex:0 0 20px;
  border:0;background:transparent;color:var(--tp-muted);
  border-radius:4px;cursor:pointer;font-size:15px;line-height:1;
}
.tp-tab-x:hover{background:rgba(179,38,30,.12);color:var(--tp-danger)}
.tp-tab.tp-pinned .tp-tab-x{display:none}

/* a tab holding unsaved work shows a warning dot in place of the cross */
.tp-tab.tp-dirty .tp-tab-x{font-size:0;position:relative}
.tp-tab.tp-dirty .tp-tab-x::before{
  content:"";width:7px;height:7px;border-radius:50%;background:var(--tp-warn);
}
.tp-tab.tp-dirty .tp-tab-x:hover::before{background:var(--tp-danger)}

.tp-tabbar-actions{
  flex:0 0 auto;display:flex;align-items:center;gap:6px;
  padding-inline:10px;border-inline-start:1px solid var(--tp-line);
}
.tp-tabtool{
  display:inline-flex;align-items:center;justify-content:center;
  width:26px;height:26px;border:0;border-radius:var(--tp-r-sm);
  background:var(--tp-line-strong);color:var(--tp-surface);cursor:pointer;
}
.tp-tabtool:hover{background:var(--tp-primary);color:var(--tp-ink-invert)}
.tp-tabtool svg{width:14px;height:14px}
.tp-tabcount{
  display:inline-flex;align-items:center;gap:6px;
  font-size:12.5px;font-weight:600;color:var(--tp-ink-2);
}
.tp-tabcount svg{width:15px;height:15px;color:var(--tp-muted)}

/* ---------- 7. PAGE HEAD + CARD ------------------------------------------ */
/* A page inside a tab: header, scrolling body, pinned command bar. */
/* Site.Master wraps the page in a server form, so that form is an extra box
   between <body> and .tp-page. Without a height on it the 100% below resolves
   against nothing and the command bar floats up under the content. */
/* Flex rather than height:100% all the way down, because ScriptManager
   injects hidden ViewState divs as siblings of .tp-page and a percentage
   chain would have to account for them. */
/* The page is positioned against the frame, NOT sized by a chain of
   percentages and flex rules through <html>, <body>, the server form and
   whatever ScriptManager injects between them. Any one of those links
   failing used to leave the command bar floating in the middle of the
   document with white space beneath it.

   inset:0 cannot fail that way: the page is exactly the frame, always. */
.tp-framed .tp-page-host{position:absolute;inset:0;margin:0;overflow:hidden}
.tp-page-host{margin:0}
.tp-page{
  position:absolute;inset:0;
  display:flex;flex-direction:column;
  background:var(--tp-bg);
}
.tp-page-head{
  /* Shallower than it was: everything in here now sits on one line, so the
     vertical padding was holding open space nothing occupies. */
  flex:0 0 auto;display:flex;align-items:center;flex-wrap:wrap;gap:10px;
  padding:7px 16px;background:var(--tp-surface);border-bottom:1px solid var(--tp-line);
}

/* Title and subtitle read as one line — "Dashboard  Welcome back" — rather
   than a stacked pair. Baseline-aligned, so the smaller subtitle sits on
   the title's line instead of floating against its cap height. */
.tp-page-titles{
  display:flex;align-items:baseline;flex-wrap:wrap;gap:9px;
  line-height:1.25;min-width:0;
}
.tp-page-title{font-size:17px;font-weight:700;letter-spacing:-.015em;margin:0}
.tp-page-sub{font-size:12.5px;color:var(--tp-muted)}
/* min-height:0 is not optional here.

   A flex item will not shrink below its CONTENT size unless told to, so
   without it a tall form pushes .tp-page past the frame and takes the
   command bar with it. Short forms hide the bug entirely — it only appears
   once the content is taller than the viewport, which is why it showed up
   on Employees and nowhere else. */
.tp-page-body{
  flex:1 1 auto;min-height:0;min-width:0;
  overflow-y:auto;overflow-x:hidden;
  -webkit-overflow-scrolling:touch;padding:16px;
}

/* overflow:hidden was here to keep the header strip inside the rounded
   corners. It also CLIPS anything that needs to escape the card — a
   multi-select panel, a date picker, an autocomplete list — and no
   z-index can climb out of a clip.

   The corners are kept by rounding the header and footer strips
   themselves instead, so the card no longer has to clip its children. */
.tp-card{
  background:var(--tp-surface);border:1px solid var(--tp-line);
  border-radius:var(--tp-r-lg);box-shadow:var(--tp-shadow);
  margin:0 auto 16px;
}
.tp-card > .tp-card-head:first-child{
  border-start-start-radius:var(--tp-r-lg);
  border-start-end-radius:var(--tp-r-lg);
}
.tp-card > *:last-child{
  border-end-start-radius:var(--tp-r-lg);
  border-end-end-radius:var(--tp-r-lg);
}
/* A table filling the last slot still needs its own corners clipped, and
   it has no children that must escape. */
.tp-card > .tp-tbl-wrap:last-child{
  overflow:hidden;
  border-end-start-radius:var(--tp-r-lg);
  border-end-end-radius:var(--tp-r-lg);
}
.tp-card > .tp-tbl-wrap.tp-tbl-scroll:last-child{overflow:auto}
.tp-card-narrow{max-width:900px}
.tp-card-wide{max-width:1200px}
/* Fixed height, and it never wraps: a list header with a search box in it
   must be exactly as tall as a form header with only a title. Whatever sits
   inside shrinks to fit rather than pushing the bar taller. */
/* WRAPS AND GROWS. It was a fixed height with nowrap and overflow:hidden,
   which is fine for a title alone — but a list header now carries a filter,
   a checkbox and a search box, and those simply did not fit. They clipped
   into each other rather than moving to a second line. */
.tp-card-head{
  display:flex;align-items:center;gap:10px;flex-wrap:wrap;
  min-height:var(--tp-cardhead-h);padding:7px 16px;
  border-bottom:1px solid var(--tp-line);
  font-size:14px;font-weight:700;letter-spacing:-.005em;
  color:var(--tp-ink);background:var(--tp-headbg);
  white-space:nowrap;
}
/* Only the title truncates. Controls beside it must stay whole — half a
   dropdown is worse than a shortened heading. */
.tp-card-head > span:first-child{
  overflow:hidden;text-overflow:ellipsis;min-width:0;
}
.tp-card-head>span:first-child{overflow:hidden;text-overflow:ellipsis}
.tp-card-head /* Header status: what the form is doing. Sits where a duplicate New button
   used to, and says the thing a button never could — a button looks the same
   whether you are creating a record or editing one. */
.tp-headstat{
  display:inline-flex;align-items:center;gap:8px;
  height:30px;padding:0 12px;border-radius:999px;
  font-size:12px;font-weight:600;letter-spacing:.02em;white-space:nowrap;
}
.tp-headstat-new{background:var(--tp-ok-soft);color:var(--tp-ok)}
.tp-headstat-edit{background:var(--tp-primary-soft);color:var(--tp-primary-dark)}
/* The record's own identifier, so "Edit" says WHICH one. */
.tp-headstat-ref{
  max-width:220px;overflow:hidden;text-overflow:ellipsis;
  padding-inline-start:8px;border-inline-start:1px solid currentColor;
  font-weight:500;opacity:.85;
}
@media (max-width:767px){
  .tp-headstat-ref{max-width:120px}
}

.tp-page-head-actions{
  display:flex;align-items:center;flex-wrap:wrap;gap:6px;
  margin-inline-start:auto;min-width:0;
}
.tp-card-head .tp-btn{min-height:30px;padding:0 14px}
.tp-card-head .tp-input{min-height:30px;padding:5px 10px;font-size:13px}

/* Inputs INSIDE a grid row. A form control is sized to be the focus of a
   field; in a table it is one cell among ten, and full height turns a
   twenty-row list into a page nobody can scan. Scoped to .tp-tbl so the
   ordinary form inputs are untouched. */
/* A number reads better right-aligned when it sits under a numeric
   heading. */
.tp-tbl .tp-input.tp-num{text-align:right}

/* The select keeps its arrow but loses the wide padding that was there to
   clear it on a full-size control. */
.tp-tbl .tp-select{padding-left:4px;padding-right:18px;background-position:right 4px center}

/* The cell sets height:var(--tp-row-h), which is 32px and WINS over any
   padding change — so the row height itself is what has to give. A cell
   holding an input drops to 26px, which the 24px control fits inside. */
.tp-tbl td:has(.tp-input),
.tp-tbl td:has(.tp-select){height:26px;padding-top:1px;padding-bottom:1px}

/* A row is only as short as its tallest cell, so the plain cells beside
   the inputs have to come down too or nothing changes. */
.tp-tbl tr:has(.tp-input) td,
.tp-tbl tr:has(.tp-select) td{height:26px}

/* The checkbox column in these grids is the same story. */
.tp-tbl tr:has(.tp-input) td.tp-sel,
.tp-tbl tr:has(.tp-select) td.tp-sel{padding-top:0;padding-bottom:0}
.tp-card-body{padding:16px}
.tp-card-foot{padding:12px 16px;border-top:1px solid var(--tp-line)}

/* legacy: cards whose fields sit directly inside the card element */
.tp-card>.tp-row{padding:16px}
.tp-card>.tp-card-head+.tp-row{padding-top:16px}

.tp-section{margin-bottom:var(--tp-gap-lg)}
.tp-section-title{
  display:flex;align-items:center;gap:9px;
  font-size:12px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
  color:var(--tp-muted);
  padding-bottom:7px;margin-bottom:var(--tp-gap);border-bottom:1px solid var(--tp-line);
}

/* ---------- 8. BUTTONS + BADGES ------------------------------------------ */
.tp-btn{
  display:inline-flex;align-items:center;justify-content:center;gap:7px;
  min-height:var(--tp-touch);padding:0 18px;
  font:600 13.5px/1 var(--tp-font);letter-spacing:-.005em;
  border:1px solid var(--tp-line-strong);border-radius:var(--tp-r);
  background:var(--tp-surface);color:var(--tp-ink);
  cursor:pointer;white-space:nowrap;
  transition:background var(--tp-t),border-color var(--tp-t),color var(--tp-t);
}
/* A mouse does not need a 44px target, and the reference sits tighter.
   Touch devices keep the full height — that limit is not negotiable. */
@media (pointer:fine){
  .tp-btn{min-height:38px}
  .tp-btn-sm{min-height:32px}
}
/* HOVER SETS ALL THREE PROPERTIES TOGETHER.

   The bug this fixes: a variant sets a dark background and white text,
   then a hover rule repaints the background light and says nothing about
   the colour — leaving white text on a white button. Every hover here
   therefore states background, border AND colour, so no variant can be
   left half-changed.

   White with a visible border and black text, whatever the button was. */
.tp-btn:hover,
.tp-btn:focus-visible:hover{
  background:var(--tp-surface);
  border-color:var(--tp-line-strong);
  color:var(--tp-ink);
  text-decoration:none;
}
.tp-btn:active{transform:translateY(1px)}
.tp-btn[disabled],.tp-btn:disabled,.tp-btn.tp-disabled{opacity:.55;pointer-events:none}

.tp-btn-primary{background:var(--tp-primary);border-color:var(--tp-primary);color:var(--tp-ink-invert)}
.tp-btn-primary:hover{
  background:var(--tp-surface);
  border-color:var(--tp-primary);
  color:var(--tp-primary-dark);
}
.tp-btn-success{background:var(--tp-ok);border-color:var(--tp-ok);color:var(--tp-ink-invert)}
.tp-btn-success:hover{filter:brightness(.93)}
.tp-btn-danger{background:var(--tp-danger);border-color:var(--tp-danger);color:var(--tp-ink-invert)}
.tp-btn-danger:hover{
  background:var(--tp-surface);
  border-color:var(--tp-danger);
  color:var(--tp-danger);
  filter:none;
}
.tp-btn-warning{background:var(--tp-warn);border-color:var(--tp-warn);color:var(--tp-ink-invert)}
.tp-btn-warning:hover{filter:brightness(.93)}
.tp-btn-ghost{border-color:transparent;background:transparent;color:var(--tp-primary)}
.tp-btn-ghost:hover{background:var(--tp-primary-soft);color:var(--tp-primary-dark)}
.tp-btn-sm{min-height:34px;padding:0 11px;font-size:12.5px}
.tp-btn-block{width:100%}
.tp-nav-btn{min-width:var(--tp-touch);padding:0 10px;font-size:15px}

.tp-chip,.tp-badge{
  display:inline-flex;align-items:center;gap:5px;
  padding:2px 9px;border-radius:999px;
  font-size:11.5px;font-weight:600;line-height:1.7;white-space:nowrap;
  background:var(--tp-primary-soft);color:var(--tp-primary);
}
.tp-badge-ok{background:var(--tp-ok-soft);color:var(--tp-ok)}
.tp-badge-info{background:var(--tp-info-soft);color:var(--tp-info)}
.tp-badge-warn{background:var(--tp-warn-soft);color:var(--tp-warn)}
.tp-badge-danger{background:var(--tp-danger-soft);color:var(--tp-danger)}
.tp-badge-muted{background:var(--tp-surface-2);color:var(--tp-muted);border:1px solid var(--tp-line)}

/* attendance status, one vocabulary across grids, tiles and reports */
/* Licence and expiry status, in the same vocabulary as attendance status. */
.tp-over{color:var(--tp-danger);font-weight:600}
.tp-expired{color:var(--tp-danger);font-weight:600}
.tp-expiring{color:var(--tp-warn);font-weight:600}

.tp-present{color:var(--tp-present);font-weight:600}
.tp-absent{color:var(--tp-absent);font-weight:600}
.tp-late{color:var(--tp-late);font-weight:600}

.tp-pill{
  display:inline-block;padding:5px 13px;border-radius:999px;
  border:1px solid var(--tp-line);background:var(--tp-surface);
  font-size:12.5px;color:var(--tp-muted);margin-bottom:16px;
}

/* ---------- 9. FIELD CHASSIS --------------------------------------------- */
/* Preferred grid for new screens. A field is a .tp-f with a span class:

     class      >=1200   992-1199   768-991   <768
     tp-c2         2         3          6       12
     tp-c3         3         4          6       12
     tp-c4         4         6          6       12
     tp-c6         6         6         12       12
     tp-c8         8        12         12       12
     tp-c12       12        12         12       12                          */
.tp-fields{
  display:grid;grid-template-columns:repeat(12,minmax(0,1fr));
  gap:var(--tp-gap);align-items:start;
}
.tp-f{grid-column:span 3;min-width:0;display:flex;flex-direction:column;gap:5px}
.tp-photo-cell{display:flex;flex-direction:column}
.tp-c1{grid-column:span 1}
.tp-c2{grid-column:span 2}
.tp-c3{grid-column:span 3}
.tp-c4{grid-column:span 4}
.tp-c5{grid-column:span 5}
.tp-c6{grid-column:span 6}
.tp-c7{grid-column:span 7}
.tp-c8{grid-column:span 8}
.tp-c9{grid-column:span 9}
.tp-c10{grid-column:span 10}
.tp-c11{grid-column:span 11}
.tp-c12{grid-column:span 12}
.tp-fill{grid-column:1/-1}

/* Forces a field to begin a new row, KEEPING its span.

   grid-column-start:1 on its own is a trap: ".tp-c6" is shorthand for
   "grid-column: span 6", which means start=span 6 / end=auto. Overriding
   the start with 1 leaves end at auto, and the field silently collapses to
   a single column. The start and the span have to be set together, so each
   width gets its own pairing below. */
.tp-newrow.tp-c1 {grid-column:1/span 1}
.tp-newrow.tp-c2 {grid-column:1/span 2}
.tp-newrow.tp-c3 {grid-column:1/span 3}
.tp-newrow.tp-c4 {grid-column:1/span 4}
.tp-newrow.tp-c5 {grid-column:1/span 5}
.tp-newrow.tp-c6 {grid-column:1/span 6}
.tp-newrow.tp-c7 {grid-column:1/span 7}
.tp-newrow.tp-c8 {grid-column:1/span 8}
.tp-newrow.tp-c9 {grid-column:1/span 9}
.tp-newrow.tp-c10{grid-column:1/span 10}
.tp-newrow.tp-c11{grid-column:1/span 11}
.tp-newrow.tp-c12{grid-column:1/span 12}

/* Legacy flex grid, still used by the screens built before the chassis. */
.tp-row{display:flex;flex-wrap:wrap;margin:0 -8px}
.tp-col{padding:0 8px;margin-bottom:var(--tp-gap);flex:1 1 100%;max-width:100%;min-width:0}
.tp-col-2{flex:0 0 50%;max-width:50%}
.tp-col-3{flex:0 0 33.3333%;max-width:33.3333%}
.tp-col-4{flex:0 0 25%;max-width:25%}

.tp-label,.tp-lbl{
  font-size:13px;font-weight:600;color:var(--tp-ink);
  display:flex;align-items:center;gap:5px;line-height:1.3;margin-bottom:5px;
}
.tp-f .tp-label{margin-bottom:0}
.tp-req{color:var(--tp-danger);font-weight:700}

input[type="text"].tp-input,input[type="password"].tp-input,
input[type="number"].tp-input,input[type="date"].tp-input,
input[type="email"].tp-input,input[type="tel"].tp-input,
input[type="url"].tp-input,
.tp-input,.tp-select,.tp-textarea,
.tp-f input[type="text"],.tp-f input[type="password"],.tp-f input[type="number"],
.tp-f input[type="date"],.tp-f input[type="email"],.tp-f input[type="tel"],
.tp-f select,.tp-f textarea{
  width:100%;min-height:var(--tp-field-h);
  padding:7px 10px;
  font:400 var(--tp-fs)/1.4 var(--tp-font);
  color:var(--tp-ink);background:var(--tp-surface);
  border:1px solid var(--tp-line-strong);border-radius:var(--tp-r);
  transition:border-color var(--tp-t),box-shadow var(--tp-t);
  appearance:none;-webkit-appearance:none;
}

/* The base rule for these controls is written as
   input[type="text"].tp-input — an element, an attribute AND a class,
   which outranks a plain ".tp-tbl .tp-input". So the selectors below
   repeat that shape rather than relying on being written later in the
   file: source order loses to specificity every time. */
.tp-tbl input[type="text"].tp-input,
.tp-tbl input[type="number"].tp-input,
.tp-tbl .tp-input,
.tp-tbl select.tp-select,
.tp-tbl .tp-select{
  min-height:24px;height:24px;padding:1px 6px;font-size:12px;
  border:1px solid transparent;background:transparent;
  border-radius:4px;box-shadow:none;width:100%
}

/* The border appears on hover and focus, so the field is still findable
   and editable — invisible is not the same as absent. */
.tp-tbl input[type="text"].tp-input:hover,
.tp-tbl input[type="number"].tp-input:hover,
.tp-tbl .tp-input:hover,
.tp-tbl select.tp-select:hover,
.tp-tbl .tp-select:hover{
  border-color:var(--tp-line);background:var(--tp-surface)
}

.tp-tbl input[type="text"].tp-input:focus,
.tp-tbl input[type="number"].tp-input:focus,
.tp-tbl .tp-input:focus,
.tp-tbl select.tp-select:focus,
.tp-tbl .tp-select:focus{
  border-color:var(--tp-primary);background:var(--tp-surface);
  box-shadow:0 0 0 2px var(--tp-primary-soft)
}

.tp-textarea,.tp-f textarea{min-height:78px;resize:vertical;line-height:1.5}
.tp-select,.tp-f select{
  padding-inline-end:30px;cursor:pointer;
  background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2 4.2 6 8.2l4-4' fill='none' stroke='%237a8699' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 10px center;background-size:12px;
}
html[dir="rtl"] .tp-select,body[dir="rtl"] .tp-select{background-position:left 10px center}

.tp-input:focus,.tp-select:focus,.tp-textarea:focus,
.tp-f input:focus,.tp-f select:focus,.tp-f textarea:focus{
  border-color:var(--tp-primary);box-shadow:var(--tp-focus);
}
.tp-input[readonly],.tp-input:disabled,.tp-select:disabled,.tp-textarea[readonly],
.tp-f input[readonly],.tp-f input[disabled],.tp-f select[disabled]{
  background:var(--tp-surface-2);color:var(--tp-ink-2);cursor:default;
}
.tp-invalid,.tp-input.tp-invalid,.tp-select.tp-invalid,.tp-textarea.tp-invalid{
  border-color:var(--tp-danger);box-shadow:0 0 0 3px var(--tp-danger-soft);
}
.tp-help{font-size:11.5px;color:var(--tp-muted);margin-top:4px}

/* Employee photo: a fixed square so a portrait and a landscape snap both
   sit in the same slot without the form reflowing. */
/* The photo spans BOTH field rows beside it. As a single cell it forced
   row one to its own height and left a band of dead space next to the
   names — the taller the photo, the worse it looked. Spanning two rows
   makes the height it needs the height the block already has. */
.tp-photo-cell{grid-row:span 2;align-self:stretch}

.tp-photo{
  width:100%;flex:1 1 auto;min-height:112px;max-height:158px;
  display:flex;align-items:center;justify-content:center;overflow:hidden;
  border:1px solid var(--tp-line-strong);border-radius:var(--tp-r);
  background:var(--tp-surface-2);margin-bottom:6px;
}
.tp-photo-img{width:100%;height:100%;object-fit:cover}
.tp-photo-none{font-size:11.5px;color:var(--tp-muted);text-align:center;padding:6px}
/* A stored path with no file behind it is a fault, not an empty slot. */
.tp-photo-missing{color:var(--tp-danger);font-weight:500}

/* File inputs: the browser default is unstyleable, so only the box around
   it is ours. */
.tp-file{
  width:100%;font:400 12.5px/1.3 var(--tp-font);color:var(--tp-ink-2);
  padding:5px;border:1px solid var(--tp-line-strong);border-radius:var(--tp-r);
  background:var(--tp-surface);
}
/* Inside a pick row it must give way to the button beside it. A file input
   has a large intrinsic minimum width and width:100% on top of that, so
   without both of these it claims the whole cell and pushes its neighbour
   off the edge. min-width:0 is what actually lets it shrink. */
.tp-pick .tp-file,
.tp-pick .tp-input,
.tp-pick .tp-select{flex:1 1 auto;width:auto;min-width:0}
.tp-pick .tp-btn{flex:0 0 auto;white-space:nowrap;padding-inline:12px}

/* Compact action beside an input. Narrow enough that the pair always fits
   in a four-column cell, so the row never has to wrap in the first place. */
/* Specificity matters here: ".tp-pick .tp-btn" above is 0,2,0 and would
   otherwise beat a lone class and put the 12px padding back. */
.tp-pick .tp-btn-dots,
.tp-btn-dots{
  flex:0 0 auto;width:38px;min-width:38px;padding:0;
  font-size:17px;line-height:1;letter-spacing:1px;
}


.tp-file::file-selector-button{
  margin-inline-end:8px;padding:4px 10px;
  border:1px solid var(--tp-line-strong);border-radius:var(--tp-r-sm);
  background:var(--tp-surface-2);color:var(--tp-ink-2);
  font:500 12px var(--tp-font);cursor:pointer;
}
.tp-file::file-selector-button:hover{background:var(--tp-primary-soft);color:var(--tp-primary)}

.tp-link{color:var(--tp-primary);text-decoration:none}
.tp-link:hover{text-decoration:underline}

.tp-badge-danger{background:var(--tp-danger-soft);color:var(--tp-danger)}

/* Map picker. A fixed height because a map with no height renders as
   nothing at all, and Leaflet cannot infer one from its container. */
.tp-map{
  width:100%;height:340px;
  border:1px solid var(--tp-line-strong);border-radius:var(--tp-r);
  background:var(--tp-surface-2);overflow:hidden;z-index:0;
}
.tp-map .leaflet-container{font:400 13px/1.4 var(--tp-font)}
.tp-geo-actions{display:flex;gap:6px;min-width:0}
.tp-geo-actions .tp-btn{flex:1 1 0;min-width:0;padding:0 8px;white-space:nowrap}
.tp-geo-actions .tp-btn svg{width:14px;height:14px;flex:0 0 14px}
/* the label on the locate button goes first when the cell is tight */
@media (max-width:1200px){
  .tp-geo-actions .tp-btn .tp-btn-text{display:none}
  .tp-geo-actions #btnGeoMine{flex:0 0 auto;padding:0 10px}
}
@media (max-width:767px){
  .tp-map{height:260px}
}

/* Read-only echo of a related record — the organization behind a client, the
   employee behind a punch. Reusable: any screen that resolves a foreign key
   can show what it resolved to. */
/* Inline warning under a field — not an error, but not neutral either. */
.tp-warn-text{color:var(--tp-warn);font-weight:500}

/* Tree rows in a flat grid. The indent is an inline-block of measured
   width rather than padding on the cell, so the row's own hover and
   selected backgrounds still run the full width behind it. */
.tp-tree-indent{display:inline-block;height:1px;vertical-align:middle}
.tp-tree-tick{color:var(--tp-muted);margin-inline-end:4px;font-size:12px}

/* Carries a title attribute. The dotted underline is the only thing that
   tells anyone a hover is worth trying — a plain cell does not. */
.tp-hastip{
  text-decoration:underline dotted var(--tp-line-strong);
  text-underline-offset:3px;cursor:help;
}

.tp-detail{
  display:flex;flex-wrap:wrap;gap:8px 26px;
  padding:11px 14px;border-radius:var(--tp-r);
  background:var(--tp-primary-soft);border:1px solid transparent;
}
.tp-detail-item{display:flex;flex-direction:column;gap:1px;min-width:0}
.tp-detail-k{
  font-size:10.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;
  color:var(--tp-muted);
}
.tp-detail-v{font-size:13px;color:var(--tp-ink);overflow:hidden;text-overflow:ellipsis}
/* search box in a card head — width belongs here, not on the page */
/* Search box in a list header: four of the twelve columns wide, with a clear
   button that only appears once there is something to clear. No Search
   button — typing filters the list on its own. */
/* Wider than it was. A third of the header is enough for "Ahmed" and not
   for a serial number or an email address, and a search you cannot read
   back is a search you retype. */
.tp-searchbox{
  position:relative;display:flex;align-items:center;
  /* 200 rather than 240: three controls in one header need the room, and
     a search box that forces a wrap on every list is worse than a slightly
     shorter one. */
  flex:1 1 40%;min-width:200px;max-width:520px;
}
.tp-searchbox .tp-input{
  width:100%;min-height:32px;padding-inline-end:30px;font-size:13px;
}
.tp-search-x{
  position:absolute;inset-inline-end:2px;
  display:none;align-items:center;justify-content:center;
  width:26px;height:26px;padding:0;
  border:0;background:transparent;color:var(--tp-muted);cursor:pointer;
  border-radius:var(--tp-r-sm);
}
.tp-searchbox.tp-has-text .tp-search-x{display:inline-flex}
.tp-search-x:hover{background:var(--tp-danger-soft);color:var(--tp-danger)}
.tp-search-x svg{width:14px;height:14px}

@media (max-width:767px){
  .tp-searchbox{flex:1 1 100%;max-width:none}
}
.tp-logo-preview{max-height:44px;max-width:190px;width:auto;border:1px solid var(--tp-line);border-radius:var(--tp-r);padding:4px;background:var(--tp-surface)}
.tp-err{font-size:11.5px;color:var(--tp-danger);font-weight:600}

/* counts and amounts align to the end, always, without a per-form rule */
.tp-input.tp-amt,.tp-input.tp-qty{text-align:end;font-weight:600}

/* Arabic fields — the global rule; forms only add data-arabic="1" */
input[data-arabic="1"],textarea[data-arabic="1"]{
  direction:rtl;text-align:right;font-family:var(--tp-font-ar);
}

/* While an Arabic field holds focus the key remap is live. Say so, or the
   user types one letter, sees Arabic appear, and wonders what happened. */
.tp-ar-on{border-color:var(--tp-primary)!important}
.tp-f.tp-ar-active .tp-lbl::after,
.tp-col.tp-ar-active .tp-label::after{
  content:"\0639";
  margin-inline-start:auto;
  display:inline-flex;align-items:center;justify-content:center;
  min-width:20px;height:17px;padding:0 5px;border-radius:4px;
  background:var(--tp-primary);color:#fff;
  font-family:var(--tp-font-ar);font-size:11px;font-weight:700;line-height:1;
}

/* code + picker: the standard lookup control */
.tp-pick{display:flex;gap:6px;width:100%;min-width:0;align-items:center}

/* flex-wrap is a safety valve, not an instruction: the row wraps only if
   the items genuinely will not fit. It previously also forced the input to
   100%, which pushed the button onto its own line whether it needed to or
   not — the opposite of the intent. */
.tp-pick{flex-wrap:wrap}
/* JOINED variant only: two inputs and a button sharing borders, as in a
   from/to range. These used to match every .tp-pick, which forced a lone
   search box to 34% and pushed its button off the edge. Opt in with
   class="tp-pick tp-pick-joined". */
.tp-pick-joined input:first-child{
  flex:0 0 34%;min-width:88px;
  border-start-end-radius:0;border-end-end-radius:0;border-inline-end:0;
}
.tp-pick-joined input:nth-child(2){flex:1 1 auto;border-radius:0;border-inline-end:0}
.tp-pick-joined .tp-btn{
  flex:0 0 auto;min-width:var(--tp-touch);padding:0 11px;
  border-start-start-radius:0;border-end-start-radius:0;
}

/* check / radio */
.tp-check{
  display:inline-flex;align-items:center;gap:8px;
  min-height:var(--tp-touch);cursor:pointer;font-size:13.5px;user-select:none;
}
.tp-check input[type="checkbox"],.tp-check input[type="radio"]{
  width:18px;height:18px;accent-color:var(--tp-primary);cursor:pointer;flex:0 0 auto;margin:0;
}
/* A control that is present but not yet usable has to look it, or the user
   clicks it, nothing happens, and the screen appears broken.
   The input carries the real signal; :has() greys the label with it where
   the browser supports it, and the input alone still reads correctly where
   it does not. */
.tp-check input:disabled{cursor:not-allowed;opacity:.5}
.tp-check:has(input:disabled){color:var(--tp-muted);cursor:default}
/* Flat group: several plain checks or radios on one line. */
.tp-checks{display:flex;flex-wrap:wrap;align-items:center;gap:2px 20px}
/* Many ticks — departments, options — laid out in columns rather than one
   long wrapping line, so they can be scanned. */
/* ---------- Token multi-select ----------
   The native <select multiple> is kept in the DOM and hidden, because it is
   what posts back. Hidden, not disabled — a disabled control submits nothing.
   Everything below is the widget drawn beside it. */
/* The plain sized list, which is what shows if the script has not upgraded it
   yet — or at all. Losing the widget must not mean losing the control. */
/* Codes that are conventionally upper case — ISO country codes and the
   like. Display only; the value is upper-cased on the server too, because
   text-transform changes what is shown, not what is posted. */
.tp-upper{text-transform:uppercase}

.tp-listbox{
  height:auto;min-height:110px;padding:4px;
  background-image:none;                 /* no caret: it does not collapse */
  overflow-y:auto;
}
.tp-listbox option{padding:5px 8px;border-radius:var(--tp-r-sm)}
.tp-listbox option:checked{background:var(--tp-primary-soft);color:var(--tp-primary-dark);font-weight:600}

.tp-multi-source{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);border:0;
}

.tp-multi{position:relative;width:100%}
/* Locked: the reason sits in the placeholder, so it must read as unusable
   rather than as an empty box somebody forgot to fill. */
.tp-multi.tp-locked .tp-multi-box{
  background:var(--tp-surface-2);border-style:dashed;cursor:not-allowed;
}
.tp-multi.tp-locked .tp-multi-input{cursor:not-allowed;color:var(--tp-muted)}
.tp-multi.tp-locked .tp-multi-caret{opacity:.4}

.tp-multi-box{
  display:flex;align-items:center;flex-wrap:wrap;gap:5px;
  min-height:var(--tp-field-h);padding:4px 4px 4px 7px;
  border:1px solid var(--tp-line-strong);border-radius:var(--tp-r);
  background:var(--tp-surface);cursor:text;
  transition:border-color var(--tp-t),box-shadow var(--tp-t);
}
.tp-multi.tp-open .tp-multi-box{border-color:var(--tp-primary);box-shadow:var(--tp-focus)}

.tp-multi-chips{display:contents}
.tp-multi-chip{
  display:inline-flex;align-items:center;gap:5px;
  max-width:100%;padding:2px 4px 2px 9px;
  background:var(--tp-primary-soft);color:var(--tp-primary-dark);
  border-radius:var(--tp-r-sm);font-size:12.5px;font-weight:500;
}
.tp-multi-chip-t{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tp-multi-chip-x{
  display:inline-flex;align-items:center;justify-content:center;
  width:17px;height:17px;flex:0 0 17px;padding:0;
  border:0;border-radius:4px;background:transparent;
  color:var(--tp-primary);cursor:pointer;font-size:14px;line-height:1;
}
.tp-multi-chip-x:hover{background:var(--tp-danger);color:#fff}

.tp-multi-input{
  flex:1 1 90px;min-width:70px;
  border:0;outline:none;background:transparent;
  font:400 13.5px/1 var(--tp-font);color:var(--tp-ink);padding:6px 2px;
}
.tp-multi-clear,.tp-multi-caret{
  display:inline-flex;align-items:center;justify-content:center;
  width:26px;height:26px;flex:0 0 26px;padding:0;
  border:0;background:transparent;color:var(--tp-muted);
  cursor:pointer;border-radius:var(--tp-r-sm);font-size:16px;line-height:1;
}
.tp-multi-clear{display:none}
.tp-multi.tp-has-value .tp-multi-clear{display:inline-flex}
.tp-multi-clear:hover{background:var(--tp-danger-soft);color:var(--tp-danger)}
.tp-multi-caret svg{width:15px;height:15px;transition:transform var(--tp-t)}
.tp-multi.tp-open .tp-multi-caret svg{transform:rotate(180deg)}

/* The OPEN widget is lifted above its neighbours. Two multi-selects side
   by side are positioned siblings, so without this the second one paints
   over the first one's open panel purely because it comes later. */
.tp-multi.tp-open{z-index:60}

.tp-multi-panel{
  display:none;position:absolute;z-index:50;inset-inline:0;top:calc(100% + 4px);
  max-height:260px;overflow-y:auto;
  background:var(--tp-surface);border:1px solid var(--tp-line);
  border-radius:var(--tp-r);box-shadow:var(--tp-shadow-lg);padding:4px;
}
.tp-multi.tp-open .tp-multi-panel{display:block}

.tp-multi-opt{
  display:flex;align-items:center;gap:10px;
  min-height:34px;padding:0 9px;border-radius:var(--tp-r-sm);
  font-size:13.5px;color:var(--tp-ink);cursor:pointer;user-select:none;
}
.tp-multi-opt:hover{background:var(--tp-primary-soft)}
.tp-multi-opt input{width:16px;height:16px;accent-color:var(--tp-primary);cursor:pointer;margin:0}
.tp-multi-all{
  font-weight:600;border-bottom:1px solid var(--tp-line);
  border-radius:var(--tp-r-sm) var(--tp-r-sm) 0 0;margin-bottom:3px;
}
.tp-multi-none{padding:14px 10px;text-align:center;color:var(--tp-muted);font-size:13px}

@media (max-width:767px){
  /* 16px stops iOS zooming the page when the filter box takes focus. */
  .tp-multi-input{font-size:16px}
  .tp-multi-opt{min-height:var(--tp-touch)}
}

.tp-checkgrid{
  display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));
  gap:2px 18px;padding:10px 12px;
}
.tp-auth-box .tp-checks{gap:0 16px}

/* Pill group: each option dressed as a selectable card. Use where the choice
   is the point of the screen, not where it is one field among many. */
.tp-radio-group{display:flex;flex-wrap:wrap;gap:8px}
.tp-radio{
  display:inline-flex;align-items:center;justify-content:center;gap:8px;
  flex:1 1 auto;min-height:var(--tp-touch);padding:0 13px;
  border:1px solid var(--tp-line-strong);border-radius:var(--tp-r);
  background:var(--tp-surface);font-size:13.5px;cursor:pointer;user-select:none;
  transition:background var(--tp-t),border-color var(--tp-t),color var(--tp-t);
}
.tp-radio input{accent-color:var(--tp-primary);margin:0;width:17px;height:17px}
.tp-radio.tp-checked{
  border-color:var(--tp-primary);background:var(--tp-primary-soft);
  color:var(--tp-primary);font-weight:600;
}

/* command bar — always the last child of .tp-page, always reachable */
/* Same height as .tp-sidebar-foot so the two line up across the split. The
   height is fixed: the BUTTONS shrink to fit, the bar never grows. */
.tp-cmdbar{
  flex:0 0 auto;height:var(--tp-footbar-h);
  display:flex;align-items:center;gap:8px;flex-wrap:nowrap;
  padding:0 16px;background:var(--tp-surface);border-top:1px solid var(--tp-line);
  box-shadow:0 -1px 3px rgba(16,24,40,.05);z-index:15;
  overflow-x:auto;scrollbar-width:none;
}
.tp-cmdbar::-webkit-scrollbar{display:none}

/* Shorter and wider than a general button: it has to clear the bar height,
   and a command reads better with room around the word. */
.tp-cmdbar .tp-btn{min-height:32px;padding:0 22px;font-size:13px}
.tp-cmdbar .tp-nav-btn{min-width:34px;padding:0 10px;font-size:14px}
/* Markup order is right, left, centre — so Tab out of the last field reaches
   Save. These put the visual order back. */
.tp-cmd-left{order:1;display:flex;gap:8px;flex:0 0 auto}
.tp-cmd-center{order:2;margin-inline:auto;display:flex;gap:4px;align-items:center}
.tp-cmd-right{order:3;display:flex;gap:8px;flex:0 0 auto}

/* ---------- 9b. FORM TEMPLATE: ENTRY / LIST PANES ------------------------ */
/* Pills, not folder tabs. The selected one is a solid block with a hard
   outline held off by a white gap — that gap is what makes it read as
   pressed. It is a border plus a box-shadow ring, NOT a soft glow: a blur
   here looks like a focus state instead of a selection. */
.tp-formtabs{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:18px}

.tp-formtab{
  min-height:38px;padding:0 18px;
  display:inline-flex;align-items:center;gap:8px;
  font-family:var(--tp-font);font-size:14px;font-weight:500;line-height:1;
  color:var(--tp-muted);
  background:#f4f5fa;
  border:3px solid transparent;          /* reserved, so nothing shifts on select */
  border-radius:6px;                     /* small radius on the fill */
  cursor:pointer;
  transition:background var(--tp-t),color var(--tp-t);
}
.tp-formtab:hover{background:#eaecf6;color:var(--tp-ink-2)}

/* Fill, then a white gap, then a hard outline. The outline picks up a slightly
   larger radius on its own because a box-shadow ring follows the border box. */
.tp-formtab.tp-active{
  color:#fff;font-weight:500;
  background:var(--tp-primary);
  border-color:#fff;
  box-shadow:0 0 0 1.5px var(--tp-primary);
}

.tp-formtab-count{
  display:inline-flex;align-items:center;justify-content:center;
  min-width:22px;height:20px;padding:0 7px;border-radius:999px;
  background:#fff;color:var(--tp-muted);
  font-size:11.5px;font-weight:700;
}
.tp-formtab.tp-active .tp-formtab-count{background:rgba(255,255,255,.22);color:#fff}

.tp-pane{display:none}
.tp-pane.tp-active{display:block}

/* the top-right action on the page header strip */
.tp-page-head-actions{margin-inline-start:auto;display:flex;gap:8px;flex-wrap:wrap;align-items:center}

@media (max-width:767px){
  .tp-formtabs{gap:8px}
  /* 38px is fine under a mouse; a finger needs the full target back. */
  .tp-formtab{flex:1 1 0;min-height:var(--tp-touch);padding:0 12px;justify-content:center}
  .tp-page-head-actions{margin-inline-start:0;width:100%}
  .tp-page-head-actions .tp-btn{flex:1}
}

/* ---------- 10. DATA GRIDS ----------------------------------------------- */
/* .tp-tbl        browse list      -> stacked cards under 768px when every
                                      <td> carries data-label
   .tp-linegrid   editable lines   -> stays a table, scrolls sideways        */
.tp-tbl-wrap{width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;background:var(--tp-surface)}

/* Stated, not inherited. A <table> does inherit from body, but the moment
   one is rendered inside anything that sets its own font — a report, an
   exported fragment, a print stylesheet — the list drifts off Roboto and
   nobody can say why. */
.tp-tbl{
  width:100%;border-collapse:collapse;font-size:13.5px;
  font-family:var(--tp-font);
  background:var(--tp-surface);
}
/* Font only. The line grid sets border-collapse:separate for its pinned
   columns, and restating the layout here would fight its own rule. */
.tp-linegrid{font-family:var(--tp-font)}

/* Digits line up in columns. Roboto has tabular figures; without asking for
   them a 1 is narrower than a 7 and a column of quantities reads ragged —
   which is exactly the column somebody is scanning for an odd value. */
.tp-tbl td.tp-num,.tp-tbl th.tp-num,
.tp-linegrid td.tp-num,.tp-linegrid th.tp-num,
.tp-tbl td.tp-sel,.tp-tbl .tp-badge{
  font-variant-numeric:tabular-nums;
  font-feature-settings:"tnum" 1;
}

/* Compact rows. A list is for scanning, and 32px shows half as many records
   again as 44px without anything becoming hard to read or hit. */
.tp-tbl th,.tp-tbl td{
  height:var(--tp-row-h);padding:0 12px;
  text-align:start;vertical-align:middle;
  border-bottom:1px solid var(--tp-line);
  /* a rule between every column, so figures in adjacent numeric columns
     cannot be read as belonging to the wrong one */
  border-right:1px solid var(--tp-line);
}
.tp-tbl th:last-child,.tp-tbl td:last-child{border-right:0}
html[dir="rtl"] .tp-tbl th,html[dir="rtl"] .tp-tbl td,
body[dir="rtl"] .tp-tbl th,body[dir="rtl"] .tp-tbl td{
  border-right:0;border-left:1px solid var(--tp-line);
}
html[dir="rtl"] .tp-tbl th:last-child,html[dir="rtl"] .tp-tbl td:last-child,
body[dir="rtl"] .tp-tbl th:last-child,body[dir="rtl"] .tp-tbl td:last-child{border-left:0}

/* Right-to-left: the action group sits on the left, so every divider moves
   to the other side. The generic rule comes first and the first-cell rule
   after it — same specificity, so order decides. */
html[dir="rtl"] .tp-tbl th.tp-act,html[dir="rtl"] .tp-tbl td.tp-act,
body[dir="rtl"] .tp-tbl th.tp-act,body[dir="rtl"] .tp-tbl td.tp-act{
  border-left:0;border-right:1px solid var(--tp-line);
}
html[dir="rtl"] .tp-tbl th.tp-act-first,html[dir="rtl"] .tp-tbl td.tp-act-first,
body[dir="rtl"] .tp-tbl th.tp-act-first,body[dir="rtl"] .tp-tbl td.tp-act-first{
  border-left:0;border-right:2px solid var(--tp-line-strong);
}

/* Same colour as the card header above it — one token drives both. */
.tp-tbl thead th{
  position:sticky;top:0;z-index:2;background:var(--tp-headbg);
  height:var(--tp-rowhead-h);
  font-size:11.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;
  color:var(--tp-ink-2);white-space:nowrap;
  border-bottom:1px solid var(--tp-line-strong);
  /* The body already rules between columns; without the same rule here the
     header floats above a gridded table and the columns stop lining up. */
  border-right:1px solid var(--tp-line);
}

/* EVERY row is white — hover and selection included. State is shown with a
   bar and with ink, never with a fill, so a printed list and a screen list
   read the same and nothing competes with the badges inside the row. */
.tp-tbl tbody tr{background:var(--tp-surface)}
/* HOVER HIGHLIGHTS THE WHOLE ROW.

   Rows rest white; hover is a transient state, not a fill somebody has to
   read data through. The bar stays as well, because on a wide table the
   tint alone is easy to lose track of when your eye is at the far column. */
.tp-tbl tbody tr:hover{
  background:var(--tp-primary-soft);
  box-shadow:inset 3px 0 0 var(--tp-line-strong);
}
/* A cell with its own background would stay white while the rest of the
   row tinted — the pinned columns of a line grid, and the heading cell of
   a mobile card. They have to follow the row. */
.tp-tbl tbody tr:hover > td{background:transparent}

/* Rows that open a record say so. */
.tp-tbl tbody tr[data-id]{cursor:pointer}
/* Selected outranks hover: hovering the row you are already on must not
   look like a different state. */
.tp-tbl tbody tr.tp-selected,
.tp-tbl tbody tr.tp-selected:hover{
  background:var(--tp-primary-soft);
  box-shadow:inset 3px 0 0 var(--tp-primary);
}
.tp-tbl tbody tr.tp-selected > td{background:transparent}
.tp-tbl tbody tr.tp-selected td{
  color:var(--tp-primary-dark);font-weight:600;
  border-bottom-color:var(--tp-primary);
}
body[dir="rtl"] .tp-tbl tbody tr:hover{box-shadow:inset -3px 0 0 var(--tp-line-strong)}
body[dir="rtl"] .tp-tbl tbody tr.tp-selected,
body[dir="rtl"] .tp-tbl tbody tr.tp-selected:hover{box-shadow:inset -3px 0 0 var(--tp-primary)}

.tp-tbl tfoot td{
  background:var(--tp-headbg);font-weight:700;
  border-top:1px solid var(--tp-line-strong);border-bottom:0;
}
.tp-tbl .tp-num,.tp-tbl th.tp-num{text-align:end}
.tp-tbl .tp-ctr,.tp-tbl th.tp-ctr{text-align:center}
/* One action per column, each as narrow as its icon. */
/* Action columns are PINNED to the trailing edge.

   A wide list scrolls sideways, and Edit and Delete are the two things
   that must never be the part that scrolled away — a row you cannot act
   on reads as a row with no actions. This is why the Devices list looked
   as though it had lost its buttons after two columns were added.

   Only inside a horizontally scrolling wrapper: a narrow table has
   nothing to scroll and a sticky cell would just pick up a shadow it
   does not need. */
.tp-tbl th.tp-act,.tp-tbl td.tp-act{
  width:1%;white-space:nowrap;text-align:center;padding:0 4px;
}
.tp-tbl-wrap th.tp-act,
.tp-tbl-wrap td.tp-act{
  position:sticky;inset-inline-end:0;z-index:3;
  background:var(--tp-surface);
}
.tp-tbl-wrap th.tp-act{background:var(--tp-headbg);z-index:4}

/* Pinned cells sit over the scrolled content, so they need their own
   separators — the neighbouring column's right border scrolls away
   underneath them. The first of the group gets a stronger one, marking
   where the fixed part begins. */
/* :first-of-type was wrong here — it means the first cell of the ROW, not
   the first of this group, so the divider landed on column one. The first
   action cell is marked in GridHelper instead, which also copes with the
   group starting at Edit, Print or Delete depending on permissions. */
/* A LEFT border on every action cell. This is the one that works with no
   rebuild: .tp-act is already on every action cell in the compiled code,
   and a left border draws the divider between the data and Edit, and
   between Edit and Delete, without needing to know which comes first. */
.tp-tbl th.tp-act,
.tp-tbl td.tp-act{
  border-left:1px solid var(--tp-line);
  border-right:0;
}

/* Once GridHelper is rebuilt the first action cell is marked, and the
   divider that separates the fixed block from the data gets weight. */
.tp-tbl th.tp-act-first,
.tp-tbl td.tp-act-first{border-left:2px solid var(--tp-line-strong)}

/* The last cell of the row keeps no trailing rule — it is the table edge. */
.tp-tbl th.tp-act:last-child,
.tp-tbl td.tp-act:last-child{border-right:0}

/* The leftmost pinned action cell casts the separator, so the group reads
   as one block rather than three floating buttons. */
.tp-tbl-wrap th.tp-act:not(:last-child),
.tp-tbl-wrap td.tp-act:not(:last-child){inset-inline-end:var(--tp-act-w,38px)}
.tp-tbl-wrap th.tp-act:nth-last-child(3),
.tp-tbl-wrap td.tp-act:nth-last-child(3){inset-inline-end:calc(var(--tp-act-w,38px) * 2)}

/* Hover and selected must reach the pinned cells too, or the row appears
   to stop short of its own buttons. */
.tp-tbl-wrap tbody tr:hover td.tp-act{background:var(--tp-primary-soft)}
.tp-tbl-wrap tbody tr.tp-selected td.tp-act{background:var(--tp-primary-soft)}
.tp-tbl th.tp-act{
  font-size:10px;letter-spacing:.04em;color:var(--tp-muted);
}
.tp-act-btn{
  display:inline-flex;align-items:center;justify-content:center;
  width:28px;height:28px;padding:0;
  border:1px solid transparent;border-radius:var(--tp-r-sm);
  background:transparent;color:var(--tp-muted);cursor:pointer;
  transition:background var(--tp-t),color var(--tp-t),border-color var(--tp-t);
}
.tp-act-btn svg{width:15px;height:15px}
.tp-act-btn:hover{border-color:currentColor}
.tp-act-edit:hover{background:var(--tp-primary-soft);color:var(--tp-primary)}
.tp-act-print:hover{background:var(--tp-info-soft);color:var(--tp-info)}
.tp-act-del:hover{background:var(--tp-danger-soft);color:var(--tp-danger)}
/* the selected row keeps its own ink, so the icons must not inherit it */
.tp-tbl tbody tr.tp-selected td.tp-act .tp-act-btn{color:var(--tp-muted)}
/* Permission matrix. Group rows are shaded so the tree reads as a tree even
   though it is a flat table. */
.tp-perm td:first-child{min-width:240px}
.tp-perm .tp-perm-group td{
  background:var(--tp-surface);font-weight:700;
  border-top:1px solid var(--tp-line-strong);
}
.tp-perm .tp-perm-item td:first-child{padding-inline-start:30px}
.tp-perm tbody tr:hover > td{background:var(--tp-primary-soft)}
.tp-perm .tp-check{min-height:0;gap:9px}
.tp-perm input[type="checkbox"]{width:17px;height:17px;accent-color:var(--tp-primary);cursor:pointer}
.tp-perm-head{display:inline-flex;flex-direction:column;align-items:center;gap:4px}
.tp-perm th{vertical-align:middle}
.tp-perm .tp-perm-box{margin:0}
/* Hooks the script binds to. Declared so they are not mistaken for typos and
   so a future style has somewhere obvious to go. */
.tp-perm-row{cursor:pointer}
.tp-perm-col{cursor:pointer}
.tp-perm-all{cursor:pointer}

/* No zebra class: alternating fills are exactly the tinted rows this rule
   forbids. Column rules already separate the data. */

.tp-linegrid-wrap{
  width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;
  border:1px solid var(--tp-line);border-radius:var(--tp-r);
}
.tp-linegrid{width:100%;min-width:940px;border-collapse:separate;border-spacing:0;font-size:13px}
.tp-linegrid th,.tp-linegrid td{padding:5px 7px;border-bottom:1px solid var(--tp-line);white-space:nowrap}
.tp-linegrid thead th{
  position:sticky;top:0;z-index:3;background:var(--tp-headbg);
  font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
  color:var(--tp-ink-2);border-bottom:1px solid var(--tp-line-strong);
}
.tp-linegrid input,.tp-linegrid select{
  width:100%;min-height:32px;padding:4px 7px;
  font:400 13px/1.3 var(--tp-font);
  border:1px solid transparent;border-radius:var(--tp-r-sm);
  background:transparent;color:var(--tp-ink);
}
.tp-linegrid input:hover,.tp-linegrid select:hover{border-color:var(--tp-line)}
.tp-linegrid input:focus,.tp-linegrid select:focus{
  border-color:var(--tp-primary);box-shadow:var(--tp-focus);background:var(--tp-surface);
}
.tp-linegrid input[readonly]{background:var(--tp-surface-2);color:var(--tp-ink-2)}
.tp-linegrid tfoot td{
  position:sticky;bottom:0;z-index:3;background:var(--tp-headbg);
  font-weight:700;text-align:end;border-top:1px solid var(--tp-line-strong);
}
/* Line grids get the same whole-row hover. The pinned columns are painted
   over their own background, or they would stay white while the row tints. */
.tp-linegrid tbody tr:hover{background:var(--tp-primary-soft)}
.tp-linegrid tbody tr:hover > td,
.tp-linegrid tbody tr:hover > td.tp-pin-start,
.tp-linegrid tbody tr:hover > td.tp-pin-end{background:var(--tp-primary-soft)}

.tp-linegrid .tp-pin-start{
  position:sticky;inset-inline-start:0;z-index:2;background:var(--tp-surface);
  width:44px;text-align:center;color:var(--tp-muted);
}
.tp-linegrid thead .tp-pin-start{z-index:4;background:var(--tp-headbg)}
.tp-linegrid .tp-pin-end{
  position:sticky;inset-inline-end:0;z-index:2;background:var(--tp-surface);
  width:48px;text-align:center;
}
.tp-linegrid thead .tp-pin-end{z-index:4;background:var(--tp-headbg)}

.tp-pager{
  display:flex;align-items:center;gap:8px;flex-wrap:wrap;
  padding:11px 16px;border-top:1px solid var(--tp-line);
  font-size:12.5px;color:var(--tp-muted);
}
.tp-pager-nav{margin-inline-start:auto;display:flex;gap:4px}

.tp-empty{padding:44px 20px;text-align:center;color:var(--tp-muted)}
.tp-empty-title{font-size:14.5px;font-weight:600;color:var(--tp-ink-2);margin-bottom:4px}

/* filter strip on report and browse screens */
.tp-filter{
  display:grid;grid-template-columns:repeat(12,minmax(0,1fr));
  gap:10px;align-items:end;
  padding:13px 16px;border-bottom:1px solid var(--tp-line);
}
.tp-filter .tp-f{gap:4px}

/* ---------- 11. KPI TILES ------------------------------------------------ */
.tp-kpi-grid{display:grid;gap:var(--tp-gap);grid-template-columns:repeat(4,1fr);margin-bottom:16px}
.tp-kpi{
  background:var(--tp-surface);border:1px solid var(--tp-line);
  border-radius:var(--tp-r-lg);padding:14px 16px;box-shadow:var(--tp-shadow);
  display:flex;flex-direction:column;gap:9px;
}
.tp-kpi-top{display:flex;align-items:center;gap:10px}
.tp-kpi-ico{
  width:36px;height:36px;flex:0 0 36px;border-radius:var(--tp-r);
  display:grid;place-items:center;color:var(--tp-ink-invert);
}
.tp-kpi-ico svg{width:18px;height:18px}
.tp-kpi-cap{
  font-size:11.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;
  color:var(--tp-muted);
}
.tp-kpi-val{font-size:22px;font-weight:600;letter-spacing:-.02em}
.tp-kpi-sub{font-size:12px;color:var(--tp-muted);margin-top:-4px}
.tp-dash-date{margin-inline-start:auto;color:var(--tp-muted);font-size:12.5px;white-space:nowrap}

/* ---------- 12. TOAST ---------------------------------------------------- */
.tp-toasts{
  position:fixed;z-index:90;inset-block-end:16px;inset-inline-end:16px;
  display:flex;flex-direction:column;gap:8px;pointer-events:none;
  padding-bottom:env(safe-area-inset-bottom);
}
.tp-toast{
  pointer-events:auto;min-width:250px;max-width:min(92vw,420px);
  display:flex;align-items:flex-start;gap:9px;
  padding:11px 14px;border-radius:var(--tp-r);
  background:var(--tp-ink);color:var(--tp-ink-invert);
  box-shadow:var(--tp-shadow-lg);font-size:13.5px;
  animation:tp-toast-in .18s ease;
}
/* Each variant states BOTH the background and the colour it needs.

   Relying on the base rule for the text colour is what makes a toast
   render white-on-white the moment anything else touches colour — the
   same fault as the buttons above. Stating both is one extra word per
   rule and cannot come apart. */
.tp-toast-success{background:var(--tp-ok);color:var(--tp-ink-invert)}
.tp-toast-error{background:var(--tp-danger);color:var(--tp-ink-invert)}
.tp-toast-warning{background:var(--tp-warn);color:var(--tp-ink-invert)}
.tp-toast-info{background:var(--tp-primary);color:var(--tp-ink-invert)}

/* The children inherit rather than guess. A span with no colour of its
   own picks up the toast's, which is the point. */
.tp-toast-msg,.tp-toast-ico,.tp-toast-x{color:inherit}
.tp-toast.tp-out{animation:tp-toast-out .18s ease forwards}
.tp-toast-ico{
  width:19px;height:19px;flex:0 0 19px;display:grid;place-items:center;
  border-radius:50%;background:rgba(255,255,255,.22);font-size:12px;font-weight:700;
}
.tp-toast-msg{flex:1 1 auto;line-height:1.45;word-break:break-word}
.tp-toast-x{cursor:pointer;opacity:.75;font-size:16px;line-height:1}
.tp-toast-x:hover{opacity:1}
@keyframes tp-toast-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
@keyframes tp-toast-out{to{opacity:0;transform:translateY(8px)}}

/* ---------- 12b. LOADING OVERLAY ----------------------------------------- */
/* THE DELAY LIVES HERE, NOT IN THE SCRIPT. A full postback starts navigating
   the moment the form submits, so the script must add .on immediately — the
   transition delay below is what stops a fast postback from flashing. */
.tp-loading{
  position:fixed;inset:0;z-index:95;
  display:flex;align-items:center;justify-content:center;
  background:rgba(255,255,255,.72);backdrop-filter:blur(1.5px);
  opacity:0;visibility:hidden;
  transition:opacity .18s ease-out .2s,visibility 0s linear .38s;
}
.tp-loading.on{opacity:1;visibility:visible;transition-delay:.2s,0s}
.tp-loading-box{
  display:flex;flex-direction:column;align-items:center;gap:12px;
  padding:22px 30px;border-radius:var(--tp-r-lg);
  background:var(--tp-surface);border:1px solid var(--tp-line);box-shadow:var(--tp-shadow-lg);
}
.tp-loading-text{font-size:13.5px;font-weight:600;color:var(--tp-ink-2)}
.tp-loading-slow{display:none;font-size:12px;color:var(--tp-muted)}
.tp-loading.long .tp-loading-slow{display:block}

.tp-rings{position:relative;width:38px;height:38px}
.tp-rings i{
  position:absolute;inset:0;border-radius:50%;
  border:2.5px solid transparent;border-top-color:var(--tp-primary);
  animation:tp-spin 1s linear infinite;
}
.tp-rings i:nth-child(2){inset:6px;border-top-color:var(--tp-info);animation-duration:1.35s}
.tp-rings i:nth-child(3){inset:12px;border-top-color:var(--tp-primary-dark);animation-duration:1.7s}
@keyframes tp-spin{to{transform:rotate(360deg)}}

body.tp-noscroll{overflow:hidden}

/* numeric fields: right aligned, because that is what a number is */
input.num,.tp-input.num{text-align:end;font-weight:600}
html[dir="rtl"] input.num,body[dir="rtl"] input.num{text-align:end}

/* a tab holding unsaved work shows a dot instead of the close cross */
.tp-tab.tp-pinned .tp-tab-x{display:none}
.tp-tab.tp-dirty .tp-tab-x{font-size:0;position:relative}
.tp-tab.tp-dirty .tp-tab-x::before{
  content:"";width:7px;height:7px;border-radius:50%;background:var(--tp-warn);
}
.tp-tab.tp-dirty .tp-tab-x:hover::before{background:var(--tp-danger)}

/* ---------- 13. MODAL ---------------------------------------------------- */
.tp-modal-back{
  position:fixed;inset:0;z-index:80;
  display:flex;align-items:center;justify-content:center;padding:14px;
  background:rgba(16,24,40,.45);
}
.tp-modal{
  position:relative;width:100%;max-width:760px;max-height:88vh;max-height:88dvh;
  display:flex;flex-direction:column;
  background:var(--tp-surface);border-radius:var(--tp-r-lg);box-shadow:var(--tp-shadow-lg);
  animation:tp-rise .2s cubic-bezier(.22,.68,.32,1) both;
}
@keyframes tp-rise{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
.tp-modal-sm{max-width:460px}
.tp-modal-lg{max-width:900px}
.tp-modal-xl{max-width:1160px}
.tp-modal-head{
  display:flex;align-items:center;gap:10px;
  padding:13px 16px;border-bottom:1px solid var(--tp-line);
  font-size:14.5px;font-weight:600;
}
.tp-modal-x{margin-inline-start:auto;cursor:pointer;color:var(--tp-muted);font-size:19px;line-height:1}
.tp-modal-x:hover{color:var(--tp-danger)}
.tp-modal-body{padding:16px;overflow:auto;-webkit-overflow-scrolling:touch;line-height:1.5}
.tp-modal-foot{
  padding:12px 16px;border-top:1px solid var(--tp-line);
  display:flex;gap:8px;justify-content:flex-end;flex:0 0 auto;
}



/* ---------- SIDEBAR COLLAPSE BUTTON ---------------------------------------
   Sits beside the logo, inside the menu it controls. A bordered square
   rather than a bare icon: it is the one control in the sidebar that acts
   on the sidebar itself, and it should not read as another menu entry.
   -------------------------------------------------------------------------- */
.tp-railbtn{
  flex:0 0 auto;
  display:none;                      /* wide screens only — see the query below */
  align-items:center;justify-content:center;
  width:34px;height:34px;margin-inline-end:10px;
  border:1px solid var(--tp-line-strong);border-radius:var(--tp-r);
  background:var(--tp-surface);color:var(--tp-primary);
  cursor:pointer;transition:background var(--tp-t),border-color var(--tp-t);
}
.tp-railbtn:hover{background:var(--tp-primary-soft);border-color:var(--tp-primary)}
.tp-railbtn:focus-visible{outline:0;box-shadow:var(--tp-focus)}

@media (min-width:993px){
  .tp-railbtn{display:inline-flex}
}

/* Collapsed, the button is all that is left in the header — the logo has
   nowhere to go at 62px, so it stands down rather than being squeezed. */
.tp-app.tp-rail .tp-railbtn,
html.tp-rail-pref .tp-railbtn{margin-inline-end:0}
.tp-app.tp-rail .tp-sidebar-logo-link,
html.tp-rail-pref .tp-sidebar-logo-link{display:none}

/* ---------- COLLAPSED RAIL ------------------------------------------------
   The sidebar folds to icons at any width, not just on a phone. On a
   1366px laptop 250px of menu is a fifth of the screen, and most of the
   time you already know where you are going.

   Labels are hidden rather than removed, so nothing reflows and the icons
   stay exactly where they were — the eye keeps its map of the menu.
   -------------------------------------------------------------------------- */
/* The pre-paint flag. Only the two properties that would visibly jump —
   the width and the labels — so a stale flag can never leave the menu in a
   state the toggle cannot undo. */
html.tp-rail-pref .tp-sidebar{
  flex:0 0 var(--tp-side-w-rail);width:var(--tp-side-w-rail);
}
html.tp-rail-pref .tp-menu-link > span:not(.tp-menu-ico),
html.tp-rail-pref .tp-menu-caret,
html.tp-rail-pref .tp-submenu{display:none}

.tp-app.tp-rail .tp-sidebar{
  flex:0 0 var(--tp-side-w-rail);width:var(--tp-side-w-rail);
}
.tp-app.tp-rail .tp-menu-link{
  padding:0;justify-content:center;gap:0;
}
/* The label span and the caret. Class names taken from what Shell.aspx.vb
   actually emits — .tp-menu-caret, not .tp-caret. */
.tp-app.tp-rail .tp-menu-link > span:not(.tp-menu-ico),
.tp-app.tp-rail .tp-menu-caret{
  display:none;
}
/* The foot holds a bare name with no element of its own, so it is hidden
   by emptying the row rather than by selecting a child that is not there. */
.tp-app.tp-rail .tp-sidebar-foot{
  font-size:0;justify-content:center;
}
/* A collapsed group cannot show its children in place — there is no room
   for them and no label to hang them under. They come back on expand. */
.tp-app.tp-rail .tp-submenu{display:none!important}
.tp-app.tp-rail .tp-menu-group > .tp-menu-link .tp-menu-caret{display:none}

/* The logo is an image with no separate text node, so it is scaled rather
   than stripped — a rail with an empty header looks broken. */
.tp-app.tp-rail .tp-sidebar-logo{
  max-width:calc(var(--tp-side-w-rail) - 14px);
  max-height:30px;
}
.tp-app.tp-rail .tp-sidebar-head{justify-content:center;padding:0}

/* The active item still has to be findable with no text to read. */
.tp-app.tp-rail .tp-menu-link.tp-active{
  background:var(--tp-primary-soft);
}


/* ---------- COMMAND TOOLBAR ------------------------------------------------
   The blue action bar on the device commands screen. Buttons that DO
   something to hardware, so they are deliberately louder than the ordinary
   command bar — and the destructive ones louder still.

   Built as a wrapping flex row rather than a fixed bar: three buttons plus
   two open menus do not fit a phone, and a toolbar that scrolls sideways is
   a toolbar whose last button nobody finds.
   -------------------------------------------------------------------------- */
.tp-toolbar{
  display:flex;flex-wrap:wrap;align-items:center;gap:8px;
  padding:10px 12px;margin-bottom:12px;
  background:var(--tp-surface);
  border:1px solid var(--tp-line);border-radius:var(--tp-r-lg);
  box-shadow:var(--tp-shadow);
}

/* The toolbar uses the application's own button, not a style of its own.
   A second button language on one screen makes the page look assembled
   from two products — and the existing .tp-btn already carries the focus
   ring, the touch target and the disabled state. */
.tp-cmdbtn{position:relative}

.tp-cmdbtn .tp-cmdcaret{
  margin-inline-start:1px;width:13px;height:13px;flex:0 0 13px;
  display:grid;place-items:center;opacity:.85;
}

/* ---------- dropdown ---------- */
.tp-cmdwrap{position:relative;display:inline-flex}

.tp-cmdmenu{
  display:none;position:absolute;z-index:70;
  inset-inline-start:0;top:calc(100% + 4px);min-width:230px;
  background:var(--tp-surface);border:1px solid var(--tp-line-strong);
  border-radius:var(--tp-r);box-shadow:var(--tp-shadow-lg);padding:4px;
}
.tp-cmdwrap.tp-open .tp-cmdmenu{display:block}

.tp-cmdmenu button{
  display:flex;align-items:center;gap:9px;width:100%;
  min-height:var(--tp-touch);padding:0 10px;
  border:0;border-radius:var(--tp-r-sm);background:transparent;
  color:var(--tp-ink);font:400 13px/1.3 var(--tp-font);text-align:start;cursor:pointer;
}
.tp-cmdmenu button:hover{background:var(--tp-primary-soft);color:var(--tp-primary-dark)}
.tp-cmdmenu button.tp-danger-item:hover{background:var(--tp-danger-soft);color:var(--tp-danger)}
.tp-cmdmenu-sep{height:1px;margin:4px 6px;background:var(--tp-line)}

/* How many rows the buttons will act on. Without it the only way to know
   is to count the ticks. */
.tp-toolbar-count{
  margin-inline-start:auto;font-size:12.5px;color:var(--tp-ink-2);white-space:nowrap;
}

@media (max-width:767px){
  /* Full width, stacked: three gradient buttons side by side on a phone
     leaves each too narrow to read its own label. */
  .tp-toolbar{padding:8px}
  .tp-cmdwrap,.tp-toolbar > .tp-btn{flex:1 1 100%}
  .tp-toolbar .tp-btn{width:100%;justify-content:center}
  .tp-cmdmenu{inset-inline:0;min-width:0}
  .tp-toolbar-count{flex:1 1 100%;margin-inline-start:0;text-align:center}
}

/* ---------- select column ---------- */
.tp-tbl th.tp-sel,.tp-tbl td.tp-sel{
  width:1%;padding:0 6px;text-align:center;
}
.tp-tbl td.tp-sel input,.tp-tbl th.tp-sel input{
  width:16px;height:16px;cursor:pointer;accent-color:var(--tp-primary);
}
/* A ticked row is doing something, so it says so — the tick alone is
   easy to lose in a wide table. */
.tp-tbl tbody tr.tp-picked{background:var(--tp-primary-soft)}
.tp-tbl tbody tr.tp-picked > td{background:transparent}

/* ---------- COMMAND LOG DIALOG -------------------------------------------- */
.tp-chiprow{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px}
.tp-chip-ok{background:var(--tp-ok-soft);color:var(--tp-ok)}
.tp-chip-info{background:var(--tp-primary-soft);color:var(--tp-primary-dark)}
.tp-chip-warn{background:var(--tp-warn-soft);color:var(--tp-warn)}

.tp-logbar{
  display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-bottom:10px;
}
.tp-logcount{font-size:13px;color:var(--tp-ink-2)}
.tp-logsize{
  display:inline-flex;align-items:center;gap:8px;margin-inline-start:auto;
  font-size:13px;color:var(--tp-ink-2);white-space:nowrap;
}
.tp-logsize .tp-select{width:auto;min-width:84px}
.tp-logpage{
  font-size:13px;color:var(--tp-ink-2);
  margin-inline-end:auto;padding-inline:8px;white-space:nowrap;
}

/* The CMD cell is a control, so it looks like one before it is clicked. */
.tp-cmdlink{display:inline-flex;align-items:center;cursor:pointer;padding:2px 6px;border-radius:var(--tp-r-sm)}
.tp-cmdlink:hover{background:var(--tp-primary-soft)}

/* Machine strings. Proportional type makes two near-identical commands
   look the same at a glance, which is exactly when it matters that they
   are not. */
.tp-cmdtext{
  font-family:var(--tp-mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace);
  font-size:12px;color:var(--tp-ink);word-break:break-word;
}

@media (max-width:767px){
  .tp-logsize{margin-inline-start:0}
  .tp-logpage{margin-inline-end:0;width:100%;text-align:center;order:-1}
}

/* ---------- SHIFT CYCLE GRID ----------------------------------------------
   One cell per day, wrapping. Auto-fill rather than fixed columns: a
   7-day cycle and a 30-day cycle are the same control, and forcing seven
   across would make the month view unreadable on a laptop.
   -------------------------------------------------------------------------- */
/* ---------- CYCLE SEQUENCE, WITH DETAIL ON DEMAND ------------------------
   The cell stays one line whatever the cycle length; the whole rotation is
   in a popup. A row that wraps to three lines to show a 31-day pattern
   makes a list of twenty patterns unreadable.
   -------------------------------------------------------------------------- */
.tp-seq{position:relative;display:inline-flex;align-items:center;max-width:100%}
.tp-seq:focus{outline:none}
.tp-seq:focus-visible{outline:2px solid var(--tp-primary);outline-offset:2px;border-radius:var(--tp-r)}

.tp-seq-line{
  display:inline-block;max-width:100%;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  font-size:11.5px;color:var(--tp-ink-2);
  font-variant-numeric:tabular-nums;
  border-bottom:1px dotted var(--tp-line-strong);
  cursor:help;
}
.tp-seq-more{
  font-size:10px;font-weight:700;color:var(--tp-primary);
  background:var(--tp-primary-soft);
  padding:0 5px;border-radius:999px;margin-inline-start:3px;
}

/* ---- the popup ---- */
.tp-seq-pop{
  position:absolute;z-index:60;top:calc(100% + 7px);inset-inline-start:0;
  display:none;
  min-width:240px;max-width:min(520px,84vw);
  padding:10px 11px;
  border:1px solid var(--tp-line);border-radius:var(--tp-r-lg);
  background:var(--tp-surface);
  box-shadow:0 10px 28px rgba(16,24,40,.18);
  text-align:start;
  /* It must never eat the pointer that is trying to reach the row under it. */
  pointer-events:none;
}

/* Hover for a mouse, focus for a keyboard, and a class for touch — where
   there is no hover at all and the popup would otherwise be unreachable. */
.tp-seq:hover .tp-seq-pop,
.tp-seq:focus-visible .tp-seq-pop,
.tp-seq.tp-seq-open .tp-seq-pop{display:block}

.tp-seq-pop-head{
  display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;
  font-size:12px;font-weight:700;color:var(--tp-ink);
  padding-bottom:7px;margin-bottom:8px;
  border-bottom:1px solid var(--tp-line);
}
.tp-seq-pop-sub{font-size:10.5px;font-weight:500;color:var(--tp-muted)}

.tp-seq-grid{
  display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:4px;
}
/* Past a fortnight, more columns — otherwise 31 days is five rows deep and
   the popup is taller than the table it floats over. */
.tp-seq-grid-wide{grid-template-columns:repeat(8,minmax(0,1fr))}

.tp-seq-cell{
  display:flex;flex-direction:column;align-items:center;gap:1px;
  padding:4px 3px;border-radius:var(--tp-r);
  background:var(--tp-surface-2);border:1px solid var(--tp-line);
  min-width:0;
}
.tp-seq-day{
  font-size:8.5px;font-weight:700;color:var(--tp-muted);
  text-transform:uppercase;letter-spacing:.03em;
}
.tp-seq-code{
  font-size:9.5px;font-weight:600;color:var(--tp-ink);
  max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}

/* An off day is quiet, so the worked days are what the eye picks out. */
.tp-seq-off{background:transparent;border-style:dashed}
.tp-seq-off .tp-seq-code{color:var(--tp-muted);font-weight:500}

/* Near the end of the table the popup would run off the edge, so the last
   rows open it to the left instead. */
.tp-tbl tr:nth-last-child(-n+3) .tp-seq-pop{top:auto;bottom:calc(100% + 7px)}

@media (max-width:767px){
  .tp-seq-pop{min-width:200px;max-width:88vw}
  .tp-seq-grid,.tp-seq-grid-wide{grid-template-columns:repeat(5,minmax(0,1fr))}
}

/* ---------- CYCLE SEGMENTED CONTROL --------------------------------------
   Three options that are always all worth seeing, so all three are shown.
   A dropdown makes somebody open it to discover what the choices are, for
   a choice with three answers.

   The selected segment is a raised white tile on a sunken grey track — the
   depth is what makes "selected" readable without colour, which matters
   where a projector or a cheap panel flattens the palette.
   -------------------------------------------------------------------------- */
/* Three columns, not a flex row with auto margins.
   margin-inline:auto centres a flex item between its NEIGHBOURS, so the
   segments sat wherever the title and the buttons left room — and moved
   every time either changed width. A grid whose outer columns are equal
   puts the middle one on the actual centre of the card. */
.tp-cyc-head-bar{
  display:grid;
  grid-template-columns:1fr auto 1fr;
  align-items:center;gap:10px;
}
.tp-cyc-head-title{font-weight:700;justify-self:start}
.tp-cyc-head-bar .tp-page-head-actions{justify-self:end}

.tp-seg{
  display:inline-flex;align-items:center;gap:2px;
  justify-self:center;
  padding:4px;
  background:var(--tp-surface-2);
  border-radius:12px;
  border:1px solid var(--tp-line);
}

.tp-seg-opt{
  appearance:none;border:0;background:transparent;
  padding:7px 18px;border-radius:9px;
  font-family:var(--tp-font);
  font-size:13px;font-weight:600;
  color:var(--tp-muted);
  cursor:pointer;white-space:nowrap;
  /* Its own touch target: this is the control that reshapes the screen. */
  min-height:34px;
  transition:color .14s,background .14s,box-shadow .14s;
}
.tp-seg-opt:hover{color:var(--tp-ink)}

.tp-seg-on{
  background:var(--tp-surface);
  color:var(--tp-ink);
  font-weight:700;
  box-shadow:0 1px 3px rgba(16,24,40,.14),0 0 0 1px rgba(16,24,40,.04);
}
.tp-seg-on:hover{color:var(--tp-ink)}

.tp-seg-opt:focus-visible{outline:2px solid var(--tp-primary);outline-offset:2px}

@media (prefers-reduced-motion:reduce){
  .tp-seg-opt{transition:none}
}

@media (max-width:900px){
  /* Stacked once there is no room for three columns. The segments go on
     their own line, still centred, rather than being squeezed between the
     title and the buttons. */
  .tp-cyc-head-bar{grid-template-columns:1fr auto;row-gap:8px}
  .tp-seg{grid-column:1 / -1;justify-self:center}
  .tp-seg-opt{padding:7px 13px;font-size:12px}
}

/* ---------- OFF-DAY SWITCH ----------------------------------------------
   A real switch rather than another chip to drag. Marking six of seven days
   as working and one as off is the commonest thing anybody does here.
   -------------------------------------------------------------------------- */
.tp-cyc-off{
  appearance:none;border:0;background:transparent;padding:0;
  margin-inline-start:auto;
  display:inline-flex;align-items:center;gap:4px;
  cursor:pointer;min-height:26px;
}
.tp-cyc-off-track{
  position:relative;display:inline-block;
  width:26px;height:15px;border-radius:999px;
  background:var(--tp-line-strong);
  transition:background .16s;
}
.tp-cyc-off-knob{
  position:absolute;top:2px;left:2px;
  width:11px;height:11px;border-radius:50%;
  background:var(--tp-surface);
  box-shadow:0 1px 2px rgba(16,24,40,.25);
  transition:transform .16s;
}
.tp-cyc-off-txt{
  font-size:8.5px;font-weight:700;letter-spacing:.05em;
  text-transform:uppercase;color:var(--tp-muted);
}

/* On means the day is OFF. Amber rather than green: a day nobody works is
   a state to notice, not a success. */
.tp-cyc-off-on .tp-cyc-off-track{background:var(--tp-warn)}
.tp-cyc-off-on .tp-cyc-off-knob{transform:translateX(11px)}
.tp-cyc-off-on .tp-cyc-off-txt{color:var(--tp-warn)}

.tp-cyc-off:focus-visible{outline:2px solid var(--tp-primary);outline-offset:2px;border-radius:999px}

[dir="rtl"] .tp-cyc-off-knob{left:auto;right:2px}
[dir="rtl"] .tp-cyc-off-on .tp-cyc-off-knob{transform:translateX(-11px)}

/* ---------- SHIFT PALETTE AND CYCLE CARDS --------------------------------
   The palette is the source; the cards are the destination. Dragging
   replaced a dropdown per card, because on a 31-day cycle the same choice
   was being made 31 times through 31 separate menus.
   -------------------------------------------------------------------------- */
.tp-palette{
  border:1px solid var(--tp-line);border-radius:var(--tp-r-lg);
  background:var(--tp-surface-2);padding:9px 10px;margin-bottom:12px;
}
.tp-palette-head{
  display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin-bottom:8px;
}
.tp-palette-title{font-size:12px;font-weight:700;color:var(--tp-ink)}

/* One row that scrolls sideways. The palette must not push the cards below
   the fold on a tablet. */
.tp-palette-rail{
  display:flex;gap:8px;overflow-x:auto;overflow-y:hidden;
  padding-bottom:4px;
  scrollbar-width:thin;
  /* pan-x, not none: the rail itself still scrolls by touch while a chip
     inside it is what gets dragged. */
  touch-action:pan-x;
  -webkit-overflow-scrolling:touch;
  overscroll-behavior-x:contain;   /* never drag the page sideways with it */
  cursor:grab;
  scroll-behavior:smooth;
}
.tp-palette-rail.tp-rail-grabbing{cursor:grabbing;scroll-behavior:auto}

.tp-chip{
  flex:0 0 auto;display:flex;flex-direction:column;gap:1px;
  min-width:132px;max-width:190px;padding:8px 11px;
  border:1px solid var(--tp-line);border-radius:var(--tp-r);
  background:var(--tp-surface);
  box-shadow:0 1px 2px rgba(16,24,40,.05);
  cursor:grab;user-select:none;
  /* The chip is dragged, so the browser must not claim the gesture. */
  touch-action:none;
  transition:box-shadow .14s,border-color .14s,transform .14s;
}
.tp-chip:hover{border-color:var(--tp-primary);box-shadow:0 2px 6px rgba(16,24,40,.10)}
.tp-chip:focus-visible{outline:2px solid var(--tp-primary);outline-offset:2px}
.tp-chip-code{font-size:12px;font-weight:700;color:var(--tp-ink)}
.tp-chip-time{font-size:10.5px;color:var(--tp-muted);white-space:nowrap}

/* The full name. Truncated rather than wrapped: a chip that grows to two
   lines pushes every other chip out of alignment along the rail. */
.tp-chip-name{
  font-size:11px;color:var(--tp-ink-2);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}

/* Tapped, waiting for a day. The keyboard and no-pointer path. */
.tp-chip-armed{
  border-color:var(--tp-primary);
  box-shadow:0 0 0 2px var(--tp-primary-soft);
}

.tp-chip-rest{background:var(--tp-surface-2);border-style:dashed}

.tp-chip-dragging{opacity:.4}

/* What follows the finger. Positioned by script, so it must not be in flow
   and must never swallow the pointer it is chasing. */
.tp-chip-ghost{
  position:fixed;z-index:9999;pointer-events:none;
  transform:translate(-50%,-50%) scale(1.04);
  box-shadow:0 8px 20px rgba(16,24,40,.22);
  border-color:var(--tp-primary);
  opacity:.95;
}

/* While dragging, nothing else should select or scroll under the finger. */
body.tp-dragging{user-select:none;-webkit-user-select:none}
body.tp-dragging .tp-cyc{cursor:copy}

/* ---------- the rail of day cards ---------- */
.tp-cycle{
  display:flex;gap:10px;
  overflow-x:auto;overflow-y:hidden;
  padding:2px 2px 8px;margin-bottom:8px;
  scrollbar-width:thin;
  /* One row, never wrapping — a cycle read across two rows loses its shape. */
  flex-wrap:nowrap;
  touch-action:pan-x;
  -webkit-overflow-scrolling:touch;
  overscroll-behavior-x:contain;
  cursor:grab;
  scroll-behavior:smooth;
  scroll-snap-type:x proximity;
}
/* While being pulled, snapping and smoothing fight the finger. */
.tp-cycle.tp-rail-grabbing{
  cursor:grabbing;scroll-behavior:auto;scroll-snap-type:none;
}

/* A single day is centred rather than stranded at the left edge of an
   empty rail. */
.tp-cycle-single{justify-content:center;overflow-x:visible}

.tp-cyc{
  flex:0 0 auto;width:168px;
  display:flex;flex-direction:column;
  border:1px solid var(--tp-line);border-radius:var(--tp-r-lg);
  background:var(--tp-surface);
  box-shadow:0 1px 3px rgba(16,24,40,.07);
  overflow:hidden;scroll-snap-align:start;
  transition:box-shadow .16s,border-color .16s,transform .16s;
}
.tp-cycle-single .tp-cyc{width:260px}

/* The card under the finger. Strong enough to be unmistakable at arm's
   length on a tablet. */
.tp-cyc-over{
  border-color:var(--tp-primary);
  box-shadow:0 0 0 3px var(--tp-primary-soft),0 6px 16px rgba(16,24,40,.14);
  transform:translateY(-2px);
}

/* A brief confirmation that the drop landed, for the case where somebody
   drops on the card they were already looking at and nothing visibly moves. */
@keyframes tp-cyc-flash{
  from{box-shadow:0 0 0 3px var(--tp-primary-soft)}
  to{box-shadow:0 1px 3px rgba(16,24,40,.07)}
}
.tp-cyc-just{animation:tp-cyc-flash .32s ease-out}

.tp-cyc-head{
  display:flex;align-items:center;gap:6px;
  padding:5px 8px 5px 10px;
  background:linear-gradient(180deg,#eaf3fd 0%,#dbeafe 100%);
  border-bottom:1px solid var(--tp-line);
}
.tp-cyc-no{font-size:14px;font-weight:800;color:var(--tp-primary)}
/* A one-day cycle has no switch, so nothing pushes the number off centre —
   it is centred explicitly instead. */
.tp-cycle-single .tp-cyc-head{justify-content:center}
.tp-cyc-dow{font-size:11px;font-weight:600;color:var(--tp-ink-2)}

.tp-cyc-shift{
  display:flex;align-items:center;gap:5px;
  min-height:34px;padding:5px 8px;
  border-bottom:1px solid var(--tp-line);
}
/* Centred only when there is nothing but the placeholder to centre. */
.tp-cyc-rest .tp-cyc-shift{justify-content:center}
.tp-cyc-badge{
  font-size:11px;font-weight:700;color:var(--tp-primary);
  background:var(--tp-primary-soft);
  padding:2px 8px;border-radius:999px;
  max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.tp-cyc-empty{font-size:11px;color:var(--tp-muted)}

/* The name under the code. One line, ellipsised, with the full text on the
   title — a card is 168px wide and most shift names are longer. */
.tp-cyc-name{
  flex:1 1 auto;min-width:0;
  font-size:10px;color:var(--tp-ink-2);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}

/* Removing an assignment is a touch target, not a hover affordance — there
   is no hover on the device this is built for. */
.tp-cyc-x{
  flex:0 0 auto;appearance:none;border:0;background:transparent;
  width:22px;height:22px;padding:0;border-radius:50%;
  display:inline-flex;align-items:center;justify-content:center;
  color:var(--tp-muted);cursor:pointer;
}
.tp-cyc-x svg{width:12px;height:12px}
.tp-cyc-x:hover{background:var(--tp-danger-soft);color:var(--tp-danger)}

/* Four rows, each carrying its own border so label and value read as one
   field rather than two things that happen to be adjacent. */
.tp-cyc-body{display:flex;flex-direction:column}
.tp-cyc-row{
  display:flex;align-items:center;justify-content:space-between;gap:6px;
  padding:6px 9px;border-bottom:1px solid var(--tp-line);
  min-height:30px;
}
.tp-cyc-row:last-child{border-bottom:0}
.tp-cyc-lbl{
  font-size:8.5px;font-weight:700;letter-spacing:.05em;
  text-transform:uppercase;color:var(--tp-muted);white-space:nowrap;
}
.tp-cyc-val{
  font-size:11.5px;font-weight:600;color:var(--tp-ink);
  font-variant-numeric:tabular-nums;white-space:nowrap;
}
.tp-cyc-dash{color:var(--tp-muted);font-weight:400}

/* A rest day is quiet rather than empty-looking: it is a deliberate choice
   and should not read as an unfinished one. */
.tp-cyc-rest{background:var(--tp-surface-2);border-style:dashed}
.tp-cyc-rest .tp-cyc-head{background:linear-gradient(180deg,#f3f4f6 0%,#e9ebef 100%)}
.tp-cyc-rest .tp-cyc-no{color:var(--tp-muted)}

@media (max-width:767px){
  .tp-cyc{width:150px}
  .tp-cycle-single .tp-cyc{width:100%;max-width:300px}
  .tp-chip{min-width:96px}
}
.tp-cycle-day{
  display:flex;flex-direction:column;gap:5px;
  padding:9px 10px;border:1px solid var(--tp-line);border-radius:var(--tp-r);
  background:var(--tp-surface);
  /* Tall enough for the shift's terms. A fixed height would clip an open
     shift, which has fewer lines than a fixed one. */
  min-height:132px;
}

/* ---------- WHAT THE CHOSEN SHIFT MEANS ----------------------------------
   Picking a shift by name asks somebody to remember the rest of it. On a
   21-day rotation that is twenty-one acts of recall, and the mistakes it
   produces only surface in a payslip.
   -------------------------------------------------------------------------- */
.tp-cycle-info{
  display:flex;flex-direction:column;gap:1px;
  margin-top:2px;padding-top:5px;
  border-top:1px dashed var(--tp-line);
  font-size:10.5px;line-height:1.45;
}
.tp-cycle-info:empty{display:none}

.tp-ci-row{display:flex;align-items:baseline;gap:5px;min-width:0}
.tp-ci-k{
  flex:0 0 auto;min-width:38px;
  color:var(--tp-muted);text-transform:uppercase;
  font-size:9px;letter-spacing:.03em;
}
.tp-ci-v{
  flex:1 1 auto;min-width:0;
  color:var(--tp-ink);font-variant-numeric:tabular-nums;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}

/* A rest day has no terms to show, so the cell stays short rather than
   holding empty space the same size as a working day. */
.tp-cycle-day.tp-rest{min-height:0}

@media (max-width:767px){
  .tp-cycle-day{min-height:0}
  .tp-ci-k{min-width:34px}
}
.tp-cycle-no{
  font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;
  color:var(--tp-muted);
}
/* A rest day recedes. On a 30-day cycle the working days are what the eye
   should find, and giving every cell equal weight defeats that. */
.tp-cycle-day.tp-rest{background:var(--tp-surface-2);border-style:dashed}
.tp-cycle-day.tp-rest .tp-cycle-no{color:var(--tp-line-strong)}

@media (max-width:767px){
  .tp-cycle{grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:8px}
}

/* Weekly off days: seven checkboxes that must read as one control. */
.tp-weekoff{display:flex;flex-wrap:wrap;gap:14px;margin-bottom:8px}

/* A rest day in the preview recedes, so the working days are what the eye
   picks out when scanning a fortnight. */
.tp-tbl tbody tr.tp-rest-row > td{color:var(--tp-muted)}
.tp-tbl tbody tr.tp-rest-row{background:var(--tp-surface-2)}

/* ---------- ROSTER HIERARCHY ----------------------------------------------
   Every assignment touching one person, with the winning one lifted. The
   losers are shown rather than hidden: "why is he on nights" is answered
   by seeing what was overridden, not only what won.
   -------------------------------------------------------------------------- */
.tp-hier{display:flex;flex-direction:column;gap:5px;margin-bottom:12px}
.tp-hier-row{
  display:flex;flex-wrap:wrap;align-items:center;gap:9px;
  padding:7px 10px;border:1px solid var(--tp-line);border-radius:var(--tp-r);
  background:var(--tp-surface-2);font-size:13px;
}
.tp-hier-row.tp-hier-win{
  background:var(--tp-ok-soft);border-color:var(--tp-ok);
}
.tp-hier-scope{
  min-width:82px;font-size:11px;font-weight:700;letter-spacing:.04em;
  text-transform:uppercase;color:var(--tp-muted);
}
.tp-hier-row.tp-hier-win .tp-hier-scope{color:var(--tp-ok)}
.tp-hier-target{flex:1 1 auto;min-width:0;color:var(--tp-ink)}
.tp-hier-what{font-family:var(--tp-mono,ui-monospace,Consolas,monospace);font-size:12px}

@media (max-width:767px){
  .tp-hier-scope{min-width:0}
}

/* ---------- ROSTER TIER HEADINGS -----------------------------------------
   A heading row each time the override level changes. The indent shows the
   order; the heading says what the order does — three steps of whitespace
   is not a label.
   -------------------------------------------------------------------------- */
.tp-tbl tbody tr.tp-tier-head > td{
  background:var(--tp-headbg);
  border-top:2px solid var(--tp-line-strong);
  height:auto;padding:7px 12px;
}
.tp-tbl tbody tr.tp-tier-head:hover > td{background:var(--tp-headbg)}
.tp-tier-name{
  font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
  color:var(--tp-ink);
}
.tp-tier-note{font-size:12px;color:var(--tp-muted);margin-inline-start:8px}

@media (max-width:767px){
  .tp-tier-note{display:none}
}

/* A department band sits inside an area band, so it is quieter — the same
   weight twice would flatten the nesting it exists to show. */
.tp-tbl tbody tr.tp-tier-head.tp-tier-sub > td{
  background:var(--tp-surface-2);
  border-top:1px solid var(--tp-line);
}
.tp-tbl tbody tr.tp-tier-head.tp-tier-sub .tp-tier-name{
  color:var(--tp-ink-2);font-size:10.5px;
}

/* ---------- ROSTER ORGANISATION TREE --------------------------------------
   Every area, its departments, and the people who differ. Nodes with no
   roster of their own are shown, not hidden — an empty branch is
   information, and hiding it is what made the earlier versions read as a
   grouped list rather than a tree.
   -------------------------------------------------------------------------- */

/* An area heads its branch, so it carries weight — but through the rule
   above it and the weight of its text, NOT a fill.
   
   The fill was a grey wash over every area row, which put the data on a
   tinted background and left the green band competing with it. Content
   reads on white; structure is shown with lines. */
.tp-orgtree tbody tr.tp-node-1 > td{
  border-top:2px solid var(--tp-line-strong);
  font-weight:600;
}

/* A node with nothing of its own recedes: it is structure, not a record. */
.tp-orgtree tbody tr.tp-node-empty > td{color:var(--tp-muted)}
.tp-orgtree tbody tr.tp-node-empty{cursor:default}
.tp-orgtree tbody tr.tp-node-empty:hover{background:transparent;box-shadow:none}
.tp-orgtree tbody tr.tp-node-empty:hover > td{background:transparent}

.tp-node-none{font-size:12.5px;font-style:italic;color:var(--tp-muted)}
.tp-node-code{font-weight:600}

/* Only rows carrying a roster are clickable, so only they look it. */
.tp-orgtree tbody tr[data-id]{cursor:pointer}

@media (max-width:767px){
  /* The indent eats the whole column on a phone; the badge still says
     which level each row is. */
  .tp-orgtree .tp-tree-indent{width:8px!important}
}

/* Rounding worked through on a real arrival and departure. Two lines
   rather than one, because the direction cuts opposite ways on the two
   punches and showing only one is how a policy gets chosen blind. */
.tp-roundex{display:flex;flex-direction:column;gap:5px}
.tp-roundex-row{display:flex;align-items:center;flex-wrap:wrap;gap:7px;font-size:12.5px}
.tp-roundex-k{min-width:72px;color:var(--tp-muted);font-weight:600}
.tp-ok-text{color:var(--tp-ok)}

/* A confirm that also takes a reason. */
.tp-prompt{display:flex;flex-direction:column;gap:9px}
.tp-prompt-msg{font-size:13.5px;color:var(--tp-ink)}

/* ---------- LEAVE COST ----------------------------------------------------
   The balance, shown before saving rather than reported afterwards.
   -------------------------------------------------------------------------- */
.tp-cost{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:10px}
.tp-cost-item{
  flex:1 1 120px;min-width:110px;
  border:1px solid var(--tp-line);border-radius:var(--tp-r);
  padding:9px 12px;background:var(--tp-surface-2);
}
.tp-cost-v{
  display:block;font-size:20px;font-weight:700;line-height:1.1;
  font-variant-numeric:tabular-nums;color:var(--tp-ink);
}
.tp-cost-k{display:block;margin-top:3px;font-size:11.5px;color:var(--tp-muted)}
.tp-cost-big .tp-cost-v{font-size:26px}
.tp-cost-note{background:transparent}
.tp-cost-good .tp-cost-v{color:var(--tp-ok)}
.tp-cost-bad{border-color:var(--tp-danger);background:var(--tp-danger-soft)}
.tp-cost-bad .tp-cost-v{color:var(--tp-danger)}

/* ---------- A DEPENDENT BLOCK ---------------------------------------------
   Fields revealed by a switch, indented under it so they read as part of
   that choice rather than as another top-level question.

   THE SPAN IS THE WHOLE POINT. .tp-fields is a 12-column grid, so a
   .tp-fields nested inside one becomes a single grid ITEM and inherits
   .tp-f's default span of 3 — a quarter of the row. Its own children then
   divide that quarter, which is how a "More than (min)" label ends up
   wrapping onto three lines beside a 40px input.
   -------------------------------------------------------------------------- */
/* ANY field grid nested inside another takes the whole row.

   .tp-fields is a 12-column grid, so a .tp-fields placed inside one becomes
   a single grid ITEM and inherits .tp-f's default span of 3 — a quarter of
   the width. Its own children then divide that quarter, which is how a
   four-field row ends up as four columns of single wrapped words.

   Written as a descendant rule rather than fixed per case: this has now
   caught two screens, and the next nested group would have hit it too. */
.tp-fields .tp-fields{grid-column:1 / -1}

.tp-subfields{
  grid-column:1 / -1;          /* redundant with the rule above, kept explicit */
  margin-inline-start:26px;
  padding:10px 0 10px 14px;
  border-inline-start:2px solid var(--tp-line);
}

/* The switch that reveals a block sits tight above it, so the two read as
   one control rather than two unrelated rows. */
.tp-switchrow{
  grid-column:1 / -1;
  display:flex;flex-direction:column;gap:3px;
  margin-top:4px;
}
.tp-switchrow .tp-help{margin-inline-start:26px}

@media (max-width:767px){
  .tp-subfields{
    margin-inline-start:10px;padding-inline-start:10px;
  }
  .tp-switchrow .tp-help{margin-inline-start:10px}
}

/* ---------- CLOCK AND TIME FORMAT SWITCH ---------------------------------
   A pair: the clock shows what the switch does to a time, so they sit
   together and read as one control.
   -------------------------------------------------------------------------- */
.tp-head-right{
  display:flex;align-items:center;gap:12px;flex-wrap:wrap;
  margin-inline-start:auto;min-width:0;
}

/* ---- the clock ---- */
.tp-clock{
  display:inline-flex;align-items:center;gap:7px;flex:0 0 auto;
  padding:3px 12px 3px 9px;
  border:1px solid var(--tp-line);border-radius:999px;
  background:linear-gradient(180deg,var(--tp-surface) 0%,var(--tp-surface-2) 100%);
  box-shadow:0 1px 2px rgba(16,24,40,.04);
  user-select:none;
}
.tp-clock-face{
  width:19px;height:19px;flex:0 0 auto;
  color:var(--tp-primary);opacity:.85;
}
/* Time and date side by side. Stacked, the clock forced the whole header
   to the height of two lines for the sake of one small date. */
.tp-clock-text{
  display:flex;align-items:baseline;gap:8px;line-height:1.1;min-width:0;
}

.tp-clock-time{
  display:flex;align-items:baseline;gap:2px;
  font-size:15px;font-weight:700;color:var(--tp-ink);
  /* Tabular figures, or the whole row shifts sideways every time a 1
     replaces a 4. */
  font-variant-numeric:tabular-nums;
  letter-spacing:.01em;
}
/* Hours and minutes carry the weight; the AM/PM suffix rides inside this
   span too, so it stays the same size as the digits rather than shrinking
   away when the format changes. */
.tp-clock-hm{font-variant-numeric:tabular-nums}

/* Seconds are quieter than the hours and minutes — they are motion, not
   information, and at equal weight they pull the eye off the time. */
.tp-clock-s{
  font-size:10px;font-weight:600;color:var(--tp-muted);
  font-variant-numeric:tabular-nums;
}
.tp-clock-s::before{content:":";opacity:.55;margin-inline-end:1px}

.tp-clock-date{
  font-size:11px;color:var(--tp-muted);white-space:nowrap;
  letter-spacing:.02em;
}
/* A hairline rather than a gap: beside the time, whitespace alone reads as
   part of the same number. */
.tp-clock-date::before{
  content:"";display:inline-block;width:1px;height:9px;
  margin-inline-end:8px;vertical-align:-1px;
  background:var(--tp-line-strong);
}

/* ---- the switch ---- */
.tp-timefmt{
  position:relative;display:inline-flex;flex:0 0 auto;
  border:1px solid var(--tp-line);border-radius:999px;
  background:var(--tp-surface-2);padding:3px;
  box-shadow:inset 0 1px 2px rgba(16,24,40,.05);
}

/* The pill slides between the two options. Positioned by class, not by
   measuring the buttons — measuring before webfonts settle produces a
   pill that jumps on first paint. */
.tp-timefmt-glider{
  position:absolute;top:3px;bottom:3px;left:3px;
  width:calc(50% - 3px);
  border-radius:999px;
  background:var(--tp-surface);
  box-shadow:0 1px 3px rgba(16,24,40,.16);
  transition:transform .18s cubic-bezier(.4,0,.2,1);
  pointer-events:none;
}
.tp-timefmt.tp-timefmt-second .tp-timefmt-glider{transform:translateX(100%)}

/* In right-to-left the pill must travel the other way, or it lands under
   the option that is not selected. */
[dir="rtl"] .tp-timefmt-glider{left:auto;right:3px}
[dir="rtl"] .tp-timefmt.tp-timefmt-second .tp-timefmt-glider{transform:translateX(-100%)}

.tp-timefmt-opt{
  position:relative;z-index:1;
  appearance:none;border:0;background:transparent;cursor:pointer;
  padding:4px 13px;border-radius:999px;
  font-family:var(--tp-font);font-size:11px;font-weight:700;
  letter-spacing:.03em;color:var(--tp-muted);
  transition:color .16s;
}
.tp-timefmt-opt:hover{color:var(--tp-ink)}
.tp-timefmt-opt.tp-timefmt-on{color:var(--tp-primary)}
.tp-timefmt-opt:focus-visible{outline:2px solid var(--tp-primary);outline-offset:2px}

/* Somebody who has asked for less motion gets the state without the
   slide. */
@media (prefers-reduced-motion:reduce){
  .tp-timefmt-glider{transition:none}
}

/* Times line up in a column when they are tabular. */
.tp-time{font-variant-numeric:tabular-nums;white-space:nowrap}
.tp-time-sep{color:var(--tp-muted)}

/* A typed time that could not be read. Marked while typing rather than
   refused at save, so it can be corrected in place. */
input[data-timeinput].tp-invalid{
  border-color:var(--tp-danger);
  background:var(--tp-danger-soft);
}

@media (max-width:1100px){
  /* The date goes before the clock does: the time is the part anybody
     actually glances at. */
  .tp-clock-date{display:none}
  .tp-clock{padding:3px 10px}
  /* Below the title, not beside it — at this width the two together push
     the clock off the row. */
  .tp-page-titles{flex-direction:column;align-items:flex-start;gap:1px}
}
@media (max-width:767px){
  /* Both go on a phone — the header has room for the title and little
     else, and the preference persists regardless. */
  .tp-clock,.tp-timefmt{display:none}
}


/* ---------- IN-ROW ICON BUTTON -------------------------------------------
   Its own class, NOT a variant of .tp-iconbtn.

   .tp-iconbtn is a 44px touch target built for the top bar. Inside a table
   row that is taller than the row itself, borderless, and swamps the cell —
   which is exactly how it looked before this existed. Sizing a shared
   class down would have shrunk the top bar instead.
   -------------------------------------------------------------------------- */
.tp-rowbtn{
  display:inline-flex;align-items:center;justify-content:center;
  width:26px;height:26px;padding:0;
  border:1px solid var(--tp-line);border-radius:var(--tp-r);
  background:var(--tp-surface);color:var(--tp-muted);
  cursor:pointer;line-height:0;
  transition:background var(--tp-t),color var(--tp-t),border-color var(--tp-t);
}
.tp-rowbtn svg{width:13px;height:13px}
.tp-rowbtn:hover{background:var(--tp-surface-2);color:var(--tp-ink)}
.tp-rowbtn:focus-visible{outline:2px solid var(--tp-primary);outline-offset:1px}

.tp-rowbtn-danger:hover{
  background:var(--tp-danger-soft);color:var(--tp-danger);border-color:var(--tp-danger);
}

/* A checkbox with no label beside it, aligned to the inputs on its row
   rather than sitting high against the label above. */
.tp-check-bare{display:flex;align-items:center;height:34px}

/* The cell holding a row button: centred, and not squeezed to the button's
   own width by the table's column sizing. */
.tp-tbl td.tp-ctr .tp-rowbtn{margin:0 auto}
.tp-tbl th.tp-ctr,.tp-tbl td.tp-ctr{white-space:nowrap}

/* The tick column in a picker table: narrow, and never the thing that
   sets the column widths. */
.tp-tbl-pick{width:44px;padding-inline:8px}
.tp-tbl .tp-cb{
  width:16px;height:16px;margin:0;cursor:pointer;
  accent-color:var(--tp-primary);
}
.tp-tbl .tp-cb:disabled{cursor:not-allowed;opacity:.5}

/* DEFECT 3: .tp-hidden was used on 30 screens and defined on none of
   them. Every "hidden" postback button in this application has been
   rendering visibly — a row of blank grey buttons above each command bar.
   Not display:none, because a display:none submit input is unreliable to
   click from script; this takes it out of the layout while leaving it
   focusable and clickable. */
.tp-hidden{
  position:absolute;width:1px;height:1px;
  padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0;
}

/* ---------- PATTERN CARD ON/OFF SWITCH -----------------------------------
   One card is on at a time: a schedule row holds ONE pattern, and two over
   the same dates would be refused as an overlap. The switch shows which,
   and — unlike a tick — makes turning it OFF a visible action rather than
   something you have to guess is possible.
   -------------------------------------------------------------------------- */
.tp-pick-sw{
  margin-inline-start:auto;
  display:inline-flex;align-items:center;flex:0 0 auto;
  pointer-events:none;   /* the whole card is the target, not this */
}
.tp-pick-sw-track{
  position:relative;display:inline-block;
  width:26px;height:15px;border-radius:999px;
  background:var(--tp-line-strong);
  transition:background .16s;
}
.tp-pick-sw-knob{
  position:absolute;top:2px;left:2px;
  width:11px;height:11px;border-radius:50%;
  background:var(--tp-surface);
  box-shadow:0 1px 2px rgba(16,24,40,.25);
  transition:transform .16s;
}
.tp-pick-sw-on .tp-pick-sw-track{background:var(--tp-primary)}
.tp-pick-sw-on .tp-pick-sw-knob{transform:translateX(11px)}

[dir="rtl"] .tp-pick-sw-knob{left:auto;right:2px}
[dir="rtl"] .tp-pick-sw-on .tp-pick-sw-knob{transform:translateX(-11px)}

/* ---------- THE ROTATION ON A PATTERN CARD -------------------------------
   "7 days, 5 worked, 2 off" describes a pattern without saying what it is.
   Two rotations can share all three numbers and mean entirely different
   things, so the card lists the days.
   -------------------------------------------------------------------------- */
.tp-pcyc{
  margin-top:6px;padding-top:6px;
  border-top:1px solid var(--tp-line);
  display:flex;flex-direction:column;gap:1px;
}
.tp-pcyc-row{
  display:flex;align-items:center;justify-content:space-between;gap:8px;
  padding:2px 0;
  font-size:10.5px;line-height:1.35;
}
.tp-pcyc-day{
  flex:0 0 34px;
  font-weight:700;color:var(--tp-muted);
  text-transform:uppercase;letter-spacing:.03em;
  font-size:9px;
}
.tp-pcyc-code{
  flex:1 1 auto;min-width:0;text-align:end;
  font-weight:600;color:var(--tp-ink);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
/* A rest day in red, the same signal the calendar uses. Consistency across
   the two screens matters more than the small loss of contrast. */
.tp-pcyc-off .tp-pcyc-code{color:var(--tp-danger);font-weight:500}
.tp-pcyc-more{
  margin-top:2px;font-size:9.5px;color:var(--tp-muted);font-style:italic;
}

/* Room for ten rows without the rail growing a scrollbar. */
.tp-pcard{width:200px}
.tp-cycle-single .tp-pcard{width:200px}

/* ---------- PATTERN CARDS AND SCHEDULE PREVIEW ---------------------------
   A pattern is a shape. A name in a dropdown does not show it, which is why
   two rotations with similar names get assigned to the wrong people.
   -------------------------------------------------------------------------- */
.tp-pcard{
  flex:0 0 auto;width:190px;
  display:flex;flex-direction:column;gap:4px;
  padding:10px 12px;
  border:1px solid var(--tp-line);border-radius:var(--tp-r-lg);
  background:var(--tp-surface);
  box-shadow:0 1px 2px rgba(16,24,40,.05);
  cursor:pointer;user-select:none;
  transition:border-color .14s,box-shadow .14s,transform .14s;
}
.tp-pcard:hover{border-color:var(--tp-primary);box-shadow:0 2px 8px rgba(16,24,40,.10)}
.tp-pcard:focus-visible{outline:2px solid var(--tp-primary);outline-offset:2px}
.tp-pcard-on{
  border-color:var(--tp-primary);
  box-shadow:0 0 0 2px var(--tp-primary-soft),0 2px 8px rgba(16,24,40,.10);
}

.tp-pcard-head{display:flex;align-items:baseline;justify-content:space-between;gap:6px}
.tp-pcard-code{font-size:12px;font-weight:700;color:var(--tp-ink)}
.tp-pcard-days{font-size:10px;color:var(--tp-muted);white-space:nowrap}
.tp-pcard-name{
  font-size:11px;color:var(--tp-ink-2);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.tp-pcard-stat{font-size:10.5px;color:var(--tp-muted)}
.tp-pcard-warn{font-size:10px;color:var(--tp-danger);font-weight:600}

/* The rotation as a strip: worked days solid, rest days hollow. Two patterns
   that read alike look different here, which is the point. */
.tp-pcard-strip{display:flex;gap:2px;align-items:center;margin-top:2px}
.tp-pstrip{
  width:8px;height:14px;border-radius:2px;
  background:var(--tp-primary);opacity:.75;
}
.tp-pstrip-off{background:transparent;border:1px dashed var(--tp-line-strong)}
.tp-pstrip-more{font-size:9px;color:var(--tp-muted);margin-inline-start:2px}

/* A header carrying no controls does not need the height of one. */
.tp-card-head-slim{
  padding-top:7px;padding-bottom:7px;min-height:0;
}
.tp-card-head-slim .tp-page-head-actions{gap:6px}

/* ---------- A HEADER THAT STAYS ON ONE LINE ------------------------------
   Title, filter, checkbox and search together. The default search box is
   flex:1 1 40% with a 200px floor, which is right when it is alone in a
   header and forces a wrap when it is the fourth thing in one.
   -------------------------------------------------------------------------- */
.tp-card-head-oneline{flex-wrap:nowrap}
.tp-card-head-oneline > span:first-child{
  flex:0 0 auto;
  /* The title gives way before the controls do: a truncated word is
     recoverable, a wrapped row of controls is not. */
  overflow:hidden;text-overflow:ellipsis;min-width:0;
}
.tp-card-head-oneline .tp-page-head-actions{flex-wrap:nowrap}
.tp-card-head-oneline .tp-select{flex:0 0 auto;width:auto}
.tp-card-head-oneline .tp-check{flex:0 0 auto;white-space:nowrap}
.tp-card-head-oneline .tp-searchbox{
  /* Narrower now that a fifth control shares the line. Still wide enough
     for a department code and a name, which is what gets typed. */
  flex:1 1 auto;min-width:120px;max-width:240px;
}
/* The two filters keep their own width rather than stretching. */
.tp-card-head-oneline .tp-select{max-width:170px}

@media (max-width:900px){
  /* Below this there genuinely is not room, and squeezing a search box to
     90px is worse than letting the row wrap. */
  .tp-card-head-oneline,
  .tp-card-head-oneline .tp-page-head-actions{flex-wrap:wrap}
  .tp-card-head-oneline .tp-searchbox{min-width:180px}
}

/* A tab left stale because it holds unsaved work. Marked rather than
   reloaded — losing a half-filled form to refresh a dropdown is a far worse
   trade than showing a stale one. The reload button takes the refresh when
   somebody is ready for it. */
.tp-tab-stale::after{
  content:"";
  width:5px;height:5px;border-radius:50%;
  background:var(--tp-warn);
  margin-inline-start:5px;flex:0 0 auto;
}

/* The dialog carries two questions. Which one is showing is decided by
   data-mode, so neither set of buttons can appear under the other's
   heading. */
.tp-lowerdlg [data-dlg="clash"]{display:none}
.tp-lowerdlg[data-mode="clash"] [data-dlg="lower"]{display:none}
.tp-lowerdlg[data-mode="clash"] [data-dlg="clash"]{display:inline-flex;gap:8px}
.tp-lowerdlg-foot > span{display:inline-flex;gap:8px;flex-wrap:wrap}

/* A special period on the list. Tinted so a target holding both reads as
   two different things rather than a duplicated row. */
/* A row carrying a special period, light green across its whole width.
   Green rather than amber: an override in force is the system working as
   intended, and amber is what this application uses for "look at this,
   something may be wrong".

   On the cells, not the row: a <tr> background is painted UNDER the <td>
   backgrounds, so any cell with its own colour would punch a hole in it. */
/* The row still tints, but lightly: the entry now carries the bar and the
   chip, and a strong fill under three other signals is one too many. */

/* The ongoing schedule sitting UNDER a special period. Quieter, because for
   those dates it is not the one in force — but still legible, since it is
   what resumes afterwards and somebody checking the handover needs to read
   it. Applied only when something actually displaces it: an ongoing
   schedule with nothing above it is the answer and must not be dimmed. */
/* ---------- ONE ROW PER SCHEDULE -----------------------------------------
   A target's schedules are separate rows; its name and headcount span them.
   A special period is a BAND across the schedule columns, so its dates and
   its buttons line up with the routine beneath and the highlight covers the
   whole run — which stacking inside cells cannot do, because each cell
   would tint on its own and the row would look striped.
   -------------------------------------------------------------------------- */

/* The spanning cells sit against the group, not the individual schedule. */
.tp-node-target{
  vertical-align:middle;
  /* White, whatever the schedule rows beside it are doing. */
  background:var(--tp-surface);
}

/* The special period is marked on the ASSIGNED TO cell only, filling it
   edge to edge and top to bottom.

   Not across the row: the dates, the cycle number and the buttons are
   data, and data reads on white. A fill running the full width put every
   figure on a tinted ground for the sake of a label that one cell can
   carry on its own. */
.tp-sch-cell{background:var(--tp-surface)}
.tp-sch-special{
  background:var(--tp-ok-soft);
  /* No padding of its own to leave a white margin inside the cell — the
     colour should reach the cell edges, or it reads as a box floating in
     a cell rather than as the cell itself. */
  padding-top:0;padding-bottom:0;
}
.tp-sch-special .tp-sch-name{font-weight:700;color:var(--tp-ok)}

/* Everything else on the row, including the target, stays white. */
.tp-row-special > td{background:var(--tp-surface)}
.tp-row-special > td.tp-sch-special{background:var(--tp-ok-soft)}

/* The routine underneath, quieter because it is not in force for those
   dates. Full strength on hover: it is what resumes, and somebody checking
   a handover has to read it. */
.tp-row-under > td{opacity:.62}
.tp-row-under:hover > td{opacity:1}

/* A continuation row loses its top border so the group reads as one block. */
.tp-row-cont > td{border-top:0}

/* ---------- one schedule in a cell ---------- */
.tp-sch{display:flex;flex-direction:column;gap:1px;min-width:0}
.tp-sch-name{
  font-weight:600;color:var(--tp-ink);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.tp-sch-src{font-size:9px;color:var(--tp-muted);font-style:italic}

/* Actions in their own column now, so they align down the table instead of
   floating after text of varying length. */
.tp-row-acts{white-space:nowrap}
.tp-row-actwrap{display:inline-flex;gap:2px}
.tp-row-acts .tp-act-btn{
  width:26px;height:26px;padding:0;
  display:inline-flex;align-items:center;justify-content:center;
}
.tp-row-acts .tp-act-btn svg{width:15px;height:15px}

/* ---------- A PICKER FOR A LIST THAT MAY BE LONG ------------------------
   A row of checkboxes works at six branches and fails at a hundred: no way
   to find one, no way to see how many are ticked, and a wall of labels to
   read. A table scrolls, searches, and counts.
   -------------------------------------------------------------------------- */
/* Branch codes in a list cell. Three fit; beyond that a count, with the
   full list on hover — a cell wide enough for six codes would push every
   other column off the screen. */
.tp-branches{display:inline-flex;gap:3px;flex-wrap:wrap;cursor:help}

/* A help line that sits beside a control rather than under it, so an
   explanation does not force a half-empty row of its own. */
.tp-help-inline{
  display:flex;align-items:center;
  min-height:var(--tp-touch);
}

.tp-picker{
  border:1px solid var(--tp-line);border-radius:var(--tp-r-lg);
  background:var(--tp-surface);overflow:hidden;
}
.tp-picker-head{
  display:flex;align-items:center;gap:10px;flex-wrap:wrap;
  padding:8px 12px;border-bottom:1px solid var(--tp-line);
  background:var(--tp-surface);
}
.tp-picker-title{font-size:12px;font-weight:700;color:var(--tp-ink)}
/* The count is of what is TICKED, so a search showing two rows cannot
   suggest two are selected. */
.tp-picker-count{font-size:11px;color:var(--tp-muted);font-weight:600}
.tp-picker-tools{
  margin-inline-start:auto;
  display:flex;align-items:center;gap:8px;flex-wrap:nowrap;
}
.tp-picker-tools .tp-searchbox{flex:0 0 auto;min-width:150px;max-width:220px}
.tp-picker-tools .tp-link{font-size:11.5px;white-space:nowrap}

/* Tall enough for eight rows, then it scrolls. Sizing it to the data would
   put the Save button below the fold on a long list. */
.tp-picker-body{max-height:264px;overflow-y:auto}
.tp-picktbl{font-size:12.5px}
.tp-picktbl td{height:32px}
.tp-picktbl thead th{position:sticky;top:0;z-index:1}
.tp-danger-text{color:var(--tp-danger);font-weight:600}
.tp-picktbl input[type="radio"]{width:16px;height:16px;accent-color:var(--tp-primary);cursor:pointer;margin:0}
.tp-pickrow{cursor:pointer}
/* The code column stays narrow so the names line up down the list rather
   than starting wherever the previous badge happened to end. */
.tp-picktbl td:nth-child(2){width:110px;white-space:nowrap}
.tp-picktbl td.tp-sel{width:38px}

/* ---------- SPECIAL SCHEDULE NOTICE --------------------------------------
   Styled as its own thing rather than as a warning. An override displacing
   the normal roster is the intended outcome, not a problem, and dressing it
   in the red used for real trouble would teach people to dismiss it.
   -------------------------------------------------------------------------- */
.tp-ovr-note{
  border:1.5px solid var(--tp-warn);
  border-radius:var(--tp-r-lg);
  background:var(--tp-warn-soft);
  padding:12px 14px;
}
.tp-ovr-note-t{
  display:block;
  font-size:13px;font-weight:800;color:var(--tp-warn);
  margin-bottom:6px;
}
.tp-ovr-note p{
  margin:0 0 6px;font-size:12.5px;color:var(--tp-ink);line-height:1.5;
}
.tp-ovr-note p:last-child{margin-bottom:0}
.tp-ovr-note-n{font-weight:600}
/* The one case where this override still loses. Marked differently because
   it is the only line here that limits what will happen. */
.tp-ovr-note-w{
  color:var(--tp-danger);font-weight:600;
  padding-top:6px;border-top:1px solid rgba(0,0,0,.08);
}

/* ---------- SPECIAL PERIOD: THE SKY --------------------------------------
   Very small stars drifting up the whole screen while the override flag is
   on. Ramadan and Hajj are why this field exists, and a quiet mark of the
   occasion is worth the few bytes.

   Fixed and behind everything at z-index 0 with pointer-events off, so it
   cannot intercept a click or scroll with the form. Two-pixel dots at low
   opacity: this sits under date fields somebody is reading carefully, and
   anything brighter would compete with the work.
   -------------------------------------------------------------------------- */
.tp-sky{
  position:fixed;inset:0;
  z-index:0;pointer-events:none;
  overflow:hidden;
  opacity:0;
  transition:opacity .5s ease;
}
body.tp-sky-on .tp-sky{opacity:1}

/* The panes are lifted above the sky. NOT .tp-page: it is absolutely
   positioned with inset:0, and switching it to relative would collapse the
   layout it holds up. The sky is already a descendant of the page, so it
   paints over the page background and under these. */
body.tp-sky-on .tp-pane,
body.tp-sky-on .tp-cmdbar{position:relative;z-index:1}

.tp-sky i{
  position:absolute;bottom:-8px;
  width:2px;height:2px;border-radius:50%;
  background:var(--tp-warn);
  box-shadow:0 0 4px rgba(245,158,11,.7);
  opacity:0;
  /* tp-skyrise, not tp-rise: that name was already taken by the login
     panel's fade-in, and a later @keyframes with the same name silently
     replaces the earlier one for EVERY element using it. The login visual
     was being sent a full viewport upward. */
  animation:tp-skyrise linear infinite;
}

/* Rising from the bottom edge and fading out before the top, so nothing
   ever piles up against the header. */
@keyframes tp-skyrise{
  0%   {transform:translateY(0) scale(.7);      opacity:0}
  12%  {opacity:.75}
  70%  {opacity:.5}
  100% {transform:translateY(-100vh) scale(1);  opacity:0}
}

/* Scattered by hand rather than evenly: a regular grid of dots reads as a
   pattern, which is the one thing this must not look like. */
.tp-sky i:nth-child(1) {left:4%;  animation-duration:15s; animation-delay:0s}
.tp-sky i:nth-child(2) {left:11%; animation-duration:19s; animation-delay:2.4s}
.tp-sky i:nth-child(3) {left:17%; animation-duration:13s; animation-delay:6.1s}
.tp-sky i:nth-child(4) {left:23%; animation-duration:21s; animation-delay:1.2s}
.tp-sky i:nth-child(5) {left:29%; animation-duration:16s; animation-delay:8.3s}
.tp-sky i:nth-child(6) {left:34%; animation-duration:18s; animation-delay:4.7s}
.tp-sky i:nth-child(7) {left:39%; animation-duration:14s; animation-delay:11s}
.tp-sky i:nth-child(8) {left:45%; animation-duration:20s; animation-delay:3.1s}
.tp-sky i:nth-child(9) {left:51%; animation-duration:17s; animation-delay:7.6s}
.tp-sky i:nth-child(10){left:56%; animation-duration:15s; animation-delay:13s}
.tp-sky i:nth-child(11){left:62%; animation-duration:22s; animation-delay:5.4s}
.tp-sky i:nth-child(12){left:67%; animation-duration:13s; animation-delay:9.8s}
.tp-sky i:nth-child(13){left:73%; animation-duration:19s; animation-delay:2s}
.tp-sky i:nth-child(14){left:78%; animation-duration:16s; animation-delay:12.2s}
.tp-sky i:nth-child(15){left:84%; animation-duration:21s; animation-delay:6.9s}
.tp-sky i:nth-child(16){left:90%; animation-duration:14s; animation-delay:10.5s}
.tp-sky i:nth-child(17){left:95%; animation-duration:18s; animation-delay:3.8s}
.tp-sky i:nth-child(18){left:8%;  animation-duration:23s; animation-delay:14.1s}
.tp-sky i:nth-child(19){left:26%; animation-duration:17s; animation-delay:16s}
.tp-sky i:nth-child(20){left:48%; animation-duration:15s; animation-delay:18.4s}
.tp-sky i:nth-child(21){left:65%; animation-duration:20s; animation-delay:15.2s}
.tp-sky i:nth-child(22){left:81%; animation-duration:16s; animation-delay:17.7s}
.tp-sky i:nth-child(23){left:14%; animation-duration:19s; animation-delay:20s}
.tp-sky i:nth-child(24){left:70%; animation-duration:14s; animation-delay:21.3s}

/* The label keeps a warm tint, so the state is legible without the motion.
   Colour and movement carrying the same message means neither is
   load-bearing on its own. */
.tp-special .tp-check > span{color:var(--tp-warn);font-weight:600}

@media (prefers-reduced-motion:reduce){
  /* No drifting at all for somebody who has asked for less motion. The
     tinted label still says the flag is on. */
  .tp-sky{display:none}
}

/* ---------- MONTH CALENDAR AND DAY PANEL --------------------------------
   A month people recognise, beside a panel that answers one question about
   one day. The strip of squares it replaces could show a code but never the
   times, and the times are what somebody is checking.
   -------------------------------------------------------------------------- */
.tp-calwrap{
  display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:14px;
  /* stretch, not start: the two panels are one object and a short month
     leaving a ragged edge beside a tall panel reads as a rendering fault. */
  align-items:stretch;
}

.tp-cal2{
  display:flex;flex-direction:column;
  border:1px solid var(--tp-line);border-radius:var(--tp-r-lg);
  background:var(--tp-surface);padding:14px 16px;
  box-shadow:0 2px 10px rgba(16,24,40,.08);
}
.tp-cal2-head{
  display:flex;align-items:center;gap:8px;margin-bottom:10px;
}
.tp-cal2-title{
  font-size:14px;font-weight:800;color:var(--tp-ink);
  margin-inline-end:auto;
}
.tp-cal2-nav{
  appearance:none;border:1px solid var(--tp-line);background:var(--tp-surface);
  width:28px;height:28px;border-radius:var(--tp-r);
  display:inline-flex;align-items:center;justify-content:center;
  color:var(--tp-ink-2);cursor:pointer;
}
.tp-cal2-nav svg{width:15px;height:15px}
.tp-cal2-nav:hover{border-color:var(--tp-primary);color:var(--tp-primary)}

.tp-cal2-grid{
  flex:1 1 auto;
  display:grid;grid-template-columns:repeat(7,minmax(0,1fr));
  /* auto for the weekday header, then equal rows sharing whatever height
     the panel beside it sets. */
  grid-template-rows:auto repeat(6,minmax(0,1fr));
  gap:3px;
}
.tp-cal2-dow{
  text-align:center;padding:4px 0 8px;
  font-size:10.5px;font-weight:600;color:var(--tp-muted);
}

.tp-cd{
  position:relative;
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  /* No fixed aspect: the rows now stretch to match the panel, and a fixed
     ratio would fight that and overflow the card. */
  min-height:38px;
  border:1px solid var(--tp-line);border-radius:var(--tp-r);
  background:var(--tp-surface);
  font-variant-numeric:tabular-nums;
  cursor:pointer;
  transition:background .12s,border-color .12s,box-shadow .12s;
}
.tp-cd:hover{
  border-color:var(--tp-primary);
  box-shadow:0 2px 6px rgba(16,24,40,.12);
}
.tp-cd-blank{
  cursor:default;background:transparent;border-color:transparent;
}
.tp-cd-blank:hover{border-color:transparent;box-shadow:none}

/* Bold and dark. The date is the thing being read, and at this size a
   mid-grey number on a light card is hard work. */
.tp-cd-n{font-size:14px;font-weight:700;color:var(--tp-ink);line-height:1}

/* A dot, not a code: at 38px a code is unreadable, and the panel beside it
   carries the detail. */
.tp-cd-dot{
  position:absolute;bottom:5px;
  width:4px;height:4px;border-radius:50%;
  background:var(--tp-primary);
}
/* A rest day is red, so a rotation's shape is legible at a glance rather
   than by reading every square. Tinted background as well as coloured text:
   colour alone is not enough for anybody who cannot distinguish it. */
.tp-cd-rest{
  background:var(--tp-danger-soft);
  border-color:var(--tp-danger-soft);
}
.tp-cd-rest .tp-cd-n{color:var(--tp-danger)}
.tp-cd-rest:hover{border-color:var(--tp-danger)}

.tp-cd-sel{
  border-color:var(--tp-primary);
  background:var(--tp-surface);
  box-shadow:0 0 0 1px var(--tp-primary);
}
.tp-cd-sel .tp-cd-n{font-weight:700;color:var(--tp-primary)}
.tp-cd-today .tp-cd-n{font-weight:700}
.tp-cd-today::after{
  content:"";position:absolute;top:4px;right:5px;
  width:3px;height:3px;border-radius:50%;background:var(--tp-warn);
}

/* Past the end of a BOUNDED schedule: shown so the range is visible, but
   not selectable — it genuinely says nothing about those days. */
.tp-cd-out{cursor:not-allowed;opacity:.32}
.tp-cd-out:hover{
  background:var(--tp-surface);border-color:var(--tp-line);box-shadow:none;
}

/* A public holiday. Amber, distinct from the red of a rest day: one is
   the rotation's own day off, the other overrides it — and on a holiday
   the pattern may still name a shift nobody works. */
.tp-cd-hol{
  background:var(--tp-warn-soft);
  border-color:var(--tp-warn);
}
.tp-cd-hol .tp-cd-n{color:var(--tp-warn);font-weight:800}
.tp-cd-hol .tp-cd-dot{background:var(--tp-warn)}

/* Before the start of an ONGOING schedule: quiet, but still selectable.
   Somebody checking a rotation should get an answer for any date, and
   "this has not started yet" is an answer. */
.tp-cd-pre{opacity:.55;background:var(--tp-surface-2)}
.tp-cd-pre .tp-cd-n{color:var(--tp-muted);font-weight:600}

/* ---------- the day panel ---------- */
.tp-dayp{
  display:flex;flex-direction:column;
  border:1.5px solid var(--tp-primary);border-radius:var(--tp-r-lg);
  background:var(--tp-surface);padding:14px 16px;
  box-shadow:0 2px 10px rgba(16,24,40,.08);
}
/* The status sits at the foot whatever the panel's height, so the two cards
   line up along the bottom as well as the top. */
.tp-dayp-status{margin-top:auto}
.tp-dayp-k{font-size:11px;color:var(--tp-muted);margin-bottom:2px}
.tp-dayp-date{font-size:19px;font-weight:800;color:var(--tp-ink);line-height:1.15}
.tp-dayp-dow{font-size:12px;color:var(--tp-ink-2);margin-bottom:12px}

.tp-dayp-band{
  padding:10px 12px;border-radius:var(--tp-r);
  background:var(--tp-primary-soft);
  font-size:13px;font-weight:800;color:var(--tp-primary);
  letter-spacing:.01em;
}
.tp-dayp-band-sub{
  display:block;font-size:10.5px;font-weight:500;color:var(--tp-ink-2);
  margin-top:2px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.tp-dayp-band-rest{background:var(--tp-surface-2);color:var(--tp-muted)}
.tp-dayp-band-out{background:var(--tp-danger-soft);color:var(--tp-danger)}
.tp-dayp-band-hol{background:var(--tp-warn-soft);color:var(--tp-warn)}

.tp-dayp-rows{margin-top:12px}
.tp-dayp-row{
  display:flex;align-items:center;justify-content:space-between;gap:10px;
  padding:9px 2px;border-bottom:1px solid var(--tp-line);
}
.tp-dayp-row:last-child{border-bottom:0}
.tp-dayp-lbl{font-size:12px;color:var(--tp-muted)}
.tp-dayp-val{
  font-size:13px;font-weight:700;color:var(--tp-ink);
  font-variant-numeric:tabular-nums;white-space:nowrap;
}
.tp-dayp-plus{
  font-size:9px;font-weight:700;color:var(--tp-warn);
  margin-inline-start:4px;
}

.tp-dayp-status{
  padding:9px 12px;border-radius:var(--tp-r);
  background:var(--tp-surface-2);
  font-size:12px;font-weight:700;color:var(--tp-ink-2);
  text-align:center;
}

@media (max-width:900px){
  /* The panel goes under the month rather than beside it. Squeezed into a
     third of a tablet it fits nothing. */
  .tp-calwrap{grid-template-columns:1fr}
}

/* ---------- the lower-level confirmation ---------- */
.tp-lowerdlg{
  position:fixed;inset:0;z-index:200;
  display:flex;align-items:center;justify-content:center;
  padding:16px;background:rgba(16,24,40,.44);
}
.tp-lowerdlg[hidden]{display:none}
.tp-lowerdlg-box{
  width:100%;max-width:620px;max-height:86vh;
  display:flex;flex-direction:column;
  border-radius:var(--tp-r-lg);background:var(--tp-surface);
  box-shadow:0 20px 48px rgba(16,24,40,.28);
  overflow:hidden;
}
.tp-lowerdlg-head{
  padding:13px 16px;font-size:14px;font-weight:700;
  border-bottom:1px solid var(--tp-line);
}
.tp-lowerdlg-body{padding:14px 16px;overflow-y:auto;font-size:12.5px}
.tp-lowerdlg-body p{margin:0 0 10px}
.tp-lowerdlg-opts{margin:12px 0 0;padding-inline-start:18px;font-size:11.5px;color:var(--tp-ink-2)}
.tp-lowerdlg-opts li{margin-bottom:5px}
.tp-lowerdlg-foot{
  display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap;
  padding:12px 16px;border-top:1px solid var(--tp-line);
  background:var(--tp-surface-2);
}

/* A card nested inside another card should not read as a second page. */
.tp-card-inset{box-shadow:none;border-color:var(--tp-line)}

/* ---------- 14. AUTH / STATE SCREENS ------------------------------------- */
/* Login, change password, error and no-access have no shell: no topbar, no
   sidebar, no tab strip. Split layout — brand panel beside the form.        */
.tp-auth{
  display:grid;grid-template-columns:1.05fr .95fr;
  min-height:100vh;min-height:100dvh;background:var(--tp-bg);
}
/* Single column, no brand panel. Used by change-password and state screens. */
.tp-auth-plain{grid-template-columns:1fr}
.tp-auth-plain .tp-auth-visual{display:none}

/* --- brand panel --- */
/* Four layers, deepest last:
     1  cool highlight top-left, so the corner is not dead flat
     2  warm violet bloom bottom-right, which stops the panel reading as
        one printed colour
     3  a subtle darkening at the very bottom, to seat the region row
     4  the base diagonal, indigo through violet
   Five stops rather than three: a two-stop gradient over a panel this tall
   bands visibly on a cheap monitor. */
.tp-auth-visual{
  position:relative;overflow:hidden;
  display:flex;flex-direction:column;justify-content:center;
  padding:56px 56px 0;
  background:
    radial-gradient(90% 70% at 12% 8%,  rgba(255,255,255,.16) 0%, transparent 60%),
    radial-gradient(80% 65% at 88% 92%, rgba(139,120,255,.38) 0%, transparent 62%),
    linear-gradient(to bottom, transparent 62%, rgba(20,16,70,.32) 100%),
    linear-gradient(152deg,
      #241f6e 0%,
      #322c8f 26%,
      #3f3ba6 52%,
      #5049bd 76%,
      #6a5fd4 100%);
  color:#fff;
}
/* faint blueprint grid, pure CSS */
.tp-auth-visual::before{
  content:"";position:absolute;inset:0;pointer-events:none;
  background-image:
    linear-gradient(rgba(255,255,255,.07) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255,255,255,.07) 1px, transparent 1px);
  background-size:44px 44px;
  mask-image:linear-gradient(to bottom,#000 0%,transparent 78%);
  -webkit-mask-image:linear-gradient(to bottom,#000 0%,transparent 78%);
}
/* the large soft arc */
.tp-auth-visual::after{
  content:"";position:absolute;pointer-events:none;
  width:620px;height:620px;border-radius:50%;
  border:1px solid rgba(255,255,255,.16);
  inset-block-start:-150px;inset-inline-end:-230px;
}
.tp-auth-visual-inner{position:relative;z-index:2;max-width:460px}

.tp-auth-brandrow{display:flex;align-items:center;gap:13px;margin-bottom:30px}
.tp-auth-mark{
  display:inline-flex;align-items:center;justify-content:center;
  width:52px;height:52px;flex:0 0 52px;border-radius:13px;
  background:rgba(255,255,255,.13);border:1px solid rgba(255,255,255,.22);
}
.tp-auth-mark svg{width:26px;height:26px}
.tp-auth-brandname{font-size:19px;font-weight:600;letter-spacing:-.01em}
.tp-auth-logo-inv{max-height:40px;width:auto}

.tp-auth-headline{
  font-size:31px;font-weight:600;line-height:1.2;letter-spacing:-.022em;margin-bottom:14px;
}
.tp-auth-lede{font-size:14.5px;line-height:1.65;color:rgba(255,255,255,.78);max-width:40ch}

.tp-auth-points{list-style:none;margin:34px 0 0;padding:0;display:grid;gap:16px}
.tp-auth-points li{display:flex;align-items:flex-start;gap:13px}
.tp-auth-pico{
  display:inline-flex;align-items:center;justify-content:center;
  width:34px;height:34px;flex:0 0 34px;border-radius:9px;
  background:rgba(255,255,255,.11);border:1px solid rgba(255,255,255,.18);
}
.tp-auth-pico svg{width:17px;height:17px}
.tp-auth-ptitle{display:block;font-size:13.5px;font-weight:600;margin-bottom:2px}
.tp-auth-ptext{display:block;font-size:12.5px;line-height:1.55;color:rgba(255,255,255,.72)}

.tp-auth-regions{
  position:relative;z-index:2;margin-top:auto;
  padding:22px 0 26px;border-top:1px solid rgba(255,255,255,.16);
  display:flex;gap:22px;font-size:12.5px;font-weight:600;color:rgba(255,255,255,.85);
}

/* --- form panel --- */
.tp-auth-panel{
  position:relative;display:flex;align-items:center;justify-content:center;
  padding:40px 28px;background:var(--tp-surface);
}
.tp-auth-box{
  width:100%;max-width:412px;padding:32px 32px 26px;
  background:var(--tp-surface);border:1px solid var(--tp-line);border-radius:14px;
  box-shadow:
    0 1px 2px rgba(16,24,40,.04),
    0 12px 28px -6px rgba(16,24,40,.10),
    0 30px 60px -20px rgba(49,45,138,.16);
  animation:tp-rise .34s cubic-bezier(.22,.68,.32,1) both;
}
.tp-auth-head{text-align:center;margin-bottom:22px}
.tp-auth-logo{max-height:44px;width:auto;margin-bottom:12px}
.tp-auth-head .tp-brand-mark{
  width:52px;height:52px;border-radius:13px;margin:0 auto 12px;
}
.tp-auth-head .tp-brand-mark svg{width:26px;height:26px}
.tp-auth-title{font-size:20px;font-weight:600;letter-spacing:-.02em;margin-bottom:4px}
.tp-auth-sub{font-size:13px;color:var(--tp-muted);line-height:1.5}

.tp-auth-box .tp-col{margin-bottom:14px;padding:0}
.tp-auth-box .tp-row{margin:0}
.tp-auth-box .tp-label{margin-bottom:5px}

/* taller, softer fields than the dense entry forms use */
.tp-auth-box .tp-input,.tp-auth-box .tp-select{
  min-height:42px;padding:9px 12px;font-size:14px;border-radius:7px;background:#fcfdff;
}
.tp-auth-box .tp-input:focus,.tp-auth-box .tp-select:focus{background:var(--tp-surface)}
.tp-auth-box .tp-btn{width:100%;min-height:44px;font-size:14px;border-radius:7px}

/* password field with a reveal button */
.tp-pass{position:relative;display:flex}
.tp-pass .tp-input{flex:1 1 auto;padding-inline-end:44px}
.tp-pass-toggle{
  position:absolute;inset-block:0;inset-inline-end:0;
  width:42px;display:inline-flex;align-items:center;justify-content:center;
  border:0;background:transparent;color:var(--tp-muted);cursor:pointer;
  border-radius:0 7px 7px 0;
}
html[dir="rtl"] .tp-pass-toggle,body[dir="rtl"] .tp-pass-toggle{border-radius:7px 0 0 7px}
.tp-pass-toggle:hover{color:var(--tp-primary)}
.tp-pass-toggle svg{width:18px;height:18px}

.tp-caps{display:none;margin-top:5px;font-size:12px;font-weight:600;color:var(--tp-warn)}
.tp-caps.tp-show{display:block}

.tp-auth-msg{
  display:none;align-items:flex-start;gap:9px;
  margin-bottom:18px;padding:11px 13px;border-radius:8px;
  font-size:13px;font-weight:500;line-height:1.45;
  background:var(--tp-danger-soft);color:var(--tp-danger);
  border:1px solid rgba(179,38,30,.18);
}
.tp-auth-msg.tp-show{display:flex;animation:tp-shake .32s ease}
@keyframes tp-shake{
  0%,100%{transform:translateX(0)}
  22%{transform:translateX(-5px)}
  55%{transform:translateX(4px)}
  80%{transform:translateX(-2px)}
}

.tp-auth-foot{
  margin-top:20px;padding-top:14px;border-top:1px solid var(--tp-line);
  display:flex;align-items:center;gap:10px;
  font-size:11.5px;color:var(--tp-muted);letter-spacing:.02em;
}
.tp-auth-foot .tp-langswitch{margin:0 0 0 auto}
html[dir="rtl"] .tp-auth-foot .tp-langswitch,
body[dir="rtl"] .tp-auth-foot .tp-langswitch{margin:0 auto 0 0}

.tp-langswitch{display:flex;gap:5px}
.tp-langswitch a{
  font-size:11.5px;font-weight:600;padding:4px 11px;border-radius:6px;
  border:1px solid var(--tp-line);color:var(--tp-muted);
}
.tp-langswitch a:hover{border-color:var(--tp-primary);color:var(--tp-primary);text-decoration:none}
.tp-langswitch a.tp-on{background:var(--tp-primary);border-color:var(--tp-primary);color:var(--tp-ink-invert)}

/* --- error / no-access states --- */
.tp-state{max-width:440px;text-align:center;margin:0 auto}
.tp-state-icon{
  width:56px;height:56px;margin:0 auto 16px;border-radius:15px;
  display:grid;place-items:center;background:var(--tp-warn-soft);color:var(--tp-warn);
}
.tp-state-icon svg{width:27px;height:27px}
.tp-state-icon.tp-state-danger{background:var(--tp-danger-soft);color:var(--tp-danger)}
.tp-state-code{font-size:52px;font-weight:700;letter-spacing:-.04em;line-height:1;color:var(--tp-primary)}
.tp-state-title{font-size:18px;font-weight:600;margin:12px 0 7px}
.tp-state-text{font-size:13.5px;color:var(--tp-ink-2);line-height:1.6}
.tp-state-actions{display:flex;gap:9px;justify-content:center;margin-top:24px;flex-wrap:wrap}

/* --- auth responsive --- */
@media (max-width:1100px){
  .tp-auth{grid-template-columns:1fr .9fr}
  .tp-auth-visual{padding:44px 40px 0}
  .tp-auth-headline{font-size:27px}
  .tp-auth-points{display:none}
}
/* below 860 the panel becomes a short banner above the form */
@media (max-width:860px){
  .tp-auth{grid-template-columns:1fr;grid-template-rows:auto 1fr}
  .tp-auth-visual{min-height:0;padding:26px 24px 0;justify-content:flex-start}
  .tp-auth-visual::after{display:none}
  .tp-auth-visual-inner{max-width:none;padding-bottom:26px}
  .tp-auth-brandrow{margin-bottom:16px}
  .tp-auth-mark{width:42px;height:42px;flex:0 0 42px;border-radius:11px}
  .tp-auth-mark svg{width:21px;height:21px}
  .tp-auth-headline{font-size:21px;margin-bottom:7px}
  .tp-auth-lede{font-size:13px;max-width:none}
  .tp-auth-regions{display:none}
  .tp-auth-panel{padding:30px 22px 34px;align-items:flex-start}
  .tp-auth-box{max-width:430px;margin:0 auto}
}
@media (max-width:520px){
  .tp-auth-visual{padding:22px 18px 0}
  .tp-auth-headline{font-size:19px}
  .tp-auth-lede{display:none}
  .tp-auth-panel{padding:22px 13px 28px}
  .tp-auth-box{padding:26px 20px 22px;border-radius:12px}
  .tp-auth-title{font-size:19px}
  /* 16px prevents iOS zooming the page on focus — never reduce this */
  .tp-auth-box .tp-input,.tp-auth-box .tp-select{font-size:16px;min-height:46px}
  .tp-state-code{font-size:42px}
}
@media (prefers-reduced-motion:reduce){
  .tp-auth-box,.tp-auth-msg.tp-show{animation:none}
}

/* ---------- 15. REPORT --------------------------------------------------- */
.tp-report-head{
  display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;
  padding:14px 16px;border-bottom:1px solid var(--tp-line);
}
.tp-report-title{font-size:16px;font-weight:600}
.tp-report-range{font-size:12.5px;color:var(--tp-muted)}
.tp-report-tools{margin-inline-start:auto;display:flex;gap:7px;flex-wrap:wrap}
.tp-viewer{width:100%;height:calc(100vh - 240px);min-height:400px;border:0;background:var(--tp-surface-2)}

.tp-grp{background:var(--tp-primary-soft);font-weight:600}
.tp-grp td{border-top:1px solid var(--tp-line-strong)}
.tp-lvl-1{padding-inline-start:16px}
.tp-lvl-2{padding-inline-start:32px}
.tp-lvl-3{padding-inline-start:48px}
.tp-total-row td{
  background:var(--tp-headbg);font-weight:700;
  border-top:1.5px solid var(--tp-line-strong);
}
.tp-bar{height:7px;border-radius:99px;background:var(--tp-surface-2);overflow:hidden}
.tp-bar span{display:block;height:100%;background:var(--tp-primary)}
.tp-bar-ok span{background:var(--tp-ok)}
.tp-bar-warn span{background:var(--tp-warn)}

/* ---------- 16. UTILITIES (the only ones permitted) ---------------------- */
.tp-hidden,.tp-hide{display:none!important}
.tp-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
.tp-muted{color:var(--tp-muted)}
.tp-strong{font-weight:600}
.tp-nowrap{white-space:nowrap}
.tp-inline-actions{display:flex;align-items:center;gap:12px;flex-wrap:wrap}


/* Content that exists only on paper. Hidden on screen rather than built
   twice, so what prints is the same markup that was verified. */
.tp-printonly{display:none}
.tp-pagerbar{display:flex;justify-content:center;padding:10px 14px}

/* Employee navigation: the position sits between the arrows, so the eye
   lands on "25 of 100" rather than on a button. */
.tp-navbar{display:flex;align-items:center;justify-content:center;gap:8px;padding:10px 14px;flex-wrap:wrap}
.tp-navpos{min-width:170px;text-align:center;font-weight:600;color:var(--tp-text)}
.tp-navbar .tp-btn:disabled{opacity:.45;cursor:default}


/* The header block on an attendance sheet: identity on the left, where
   they sit on the right, in the two columns the printed sheet is read in. */
.tp-sheethead{
  display:grid;grid-template-columns:1fr 1fr;gap:4px 24px;
  padding:4px 0 14px;border-bottom:1px solid var(--tp-line);margin-bottom:12px
}
.tp-sheetpair{display:flex;gap:10px;align-items:baseline}
.tp-sheetpair .tp-lbl{min-width:110px;margin:0}

/* A day nobody was scheduled to work. Dimmed rather than coloured: it is
   not a problem, it is just not a working day. */
.tp-row-rest > td{background:var(--tp-surface-2);opacity:.72}

@media (max-width:720px){
  .tp-sheethead{grid-template-columns:1fr}
}
.tp-icon-btn{font-size:12px;font-weight:600;text-decoration:none;padding:2px 6px}

/* A pair with only one half. Tinted rather than left to be spotted by
   reading every row: it is the row somebody opened this list to find. */
.tp-row-warn > td{background:var(--tp-warn-soft)}
.tp-right{text-align:end}
.tp-center{text-align:center}
.tp-mt{margin-top:var(--tp-gap)}
.tp-mb{margin-bottom:var(--tp-gap)}

/* ---------- 17. RESPONSIVE ----------------------------------------------- */
@media (max-width:1400px){
  .tp-kpi-grid{grid-template-columns:repeat(3,1fr)}
}
@media (max-width:1200px){
  :root{--tp-side-w:220px}
}
@media (max-width:1199px){
  /* Spans are NOT rewritten here. The authored 12-column layout holds all
     the way down to 992px — reassigning a span mid-range is what turns a
     2 + 5 + 5 row into 3 + 6 + 6 and breaks it. */
  .tp-col-4{flex:0 0 50%;max-width:50%}
}
@media (max-width:992px){
  .tp-burger{display:inline-flex}
  /* The drawer stops above the footer rather than covering it. */
  .tp-sidebar{
    position:fixed;inset-block-start:0;inset-block-end:var(--tp-appfoot-h);
    inset-inline-start:0;
    transform:translateX(-102%);box-shadow:var(--tp-shadow-lg);
  }
  html[dir="rtl"] .tp-sidebar,body[dir="rtl"] .tp-sidebar{transform:translateX(102%)}
  .tp-sidebar.tp-open{transform:none}
  .tp-kpi-grid{grid-template-columns:repeat(2,1fr)}
}
@media (max-width:991px){
  /* Spans are NOT remapped. A row authored as 2+5+5 must stay 2+5+5:
     remapping each span on its own makes the row add to more than twelve
     and it wraps. The grid keeps all twelve columns down to the phone
     breakpoint below, where every field goes full width at once. */
  .tp-col-3{flex:0 0 50%;max-width:50%}
}
@media (max-width:768px){
  .tp-page-body{padding:11px}
  .tp-card{border-radius:var(--tp-r)}
  .tp-card-body{padding:13px}
  .tp-card>.tp-row{padding:13px}
  .tp-tab{max-width:150px;padding-inline:10px;font-size:12.5px}
  .tp-tab-x{width:26px;height:26px}
  .tp-topbar-item.tp-hide-sm,.tp-userbox.tp-hide-sm{display:none}
  .tp-toasts{inset-inline:10px;inset-block-end:10px}
  .tp-toast{max-width:none}
}
@media (max-width:767px){
  .tp-fields,.tp-filter{grid-template-columns:1fr;gap:11px}
  .tp-f,.tp-c1,.tp-c2,.tp-c3,.tp-c4,.tp-c5,.tp-c6,
  .tp-c7,.tp-c8,.tp-c9,.tp-c10,.tp-c11,.tp-c12{grid-column:span 1}
  /* One column means there are no two rows to span. */
  .tp-photo-cell{grid-row:auto}
  .tp-photo{max-height:200px}
  /* Repeated at the same specificity as the .tp-newrow pairings above, and
     later in the file, so a row break does not survive into one column. */
  .tp-f.tp-newrow{grid-column:1/span 1}
  .tp-col,.tp-col-2,.tp-col-3,.tp-col-4{flex:0 0 100%;max-width:100%}

  /* 16px kills the iOS auto-zoom on focus. Non-negotiable. */
  .tp-input,.tp-select,.tp-textarea,
  .tp-f input,.tp-f select,.tp-f textarea{font-size:16px;min-height:var(--tp-touch)}

  /* A phone needs the full 44px target and may wrap, so the fixed height
     is released here — it is a desktop alignment concern, not a rule. */
  .tp-cmdbar{
    height:auto;flex-wrap:wrap;
    padding:10px 12px calc(10px + env(safe-area-inset-bottom));
  }
  .tp-cmdbar .tp-btn{min-height:var(--tp-touch)}
  .tp-cmd-left,.tp-cmd-right{flex:1 1 100%}
  .tp-cmd-left .tp-btn,.tp-cmd-right .tp-btn{flex:1}
  .tp-cmd-center{order:0;flex:1 1 100%;justify-content:center;margin:0 0 4px}
  .tp-pick-joined input:first-child{flex:0 0 40%}

  /* Browse tables become cards. timepro.js fills data-label from the
     header on every table, so this always reads correctly. Add
     .tp-tbl-scroll to the wrapper to keep a wide grid scrolling instead. */
  .tp-tbl-wrap:not(.tp-tbl-scroll) .tp-tbl,
  .tp-tbl-wrap:not(.tp-tbl-scroll) thead,
  .tp-tbl-wrap:not(.tp-tbl-scroll) tbody,
  .tp-tbl-wrap:not(.tp-tbl-scroll) tfoot,
  .tp-tbl-wrap:not(.tp-tbl-scroll) tr,
  .tp-tbl-wrap:not(.tp-tbl-scroll) th,
  .tp-tbl-wrap:not(.tp-tbl-scroll) td{display:block}
  .tp-tbl-wrap:not(.tp-tbl-scroll){overflow:visible}
  .tp-tbl-wrap:not(.tp-tbl-scroll) thead{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}
  .tp-tbl-wrap:not(.tp-tbl-scroll) tbody tr{
    border:1px solid var(--tp-line);border-radius:var(--tp-r);
    margin-bottom:9px;padding:3px 0;background:var(--tp-surface);box-shadow:var(--tp-shadow);
  }
  /* Card view gets the same whole-row highlight. The heading cell has its
     own background and enough specificity to keep it, so it is overridden
     here rather than relying on the base rule to reach it. */
  .tp-tbl-wrap:not(.tp-tbl-scroll) tbody tr:hover{background:var(--tp-primary-soft)}
  .tp-tbl-wrap:not(.tp-tbl-scroll) tbody tr:hover td:first-child,
  .tp-tbl-wrap:not(.tp-tbl-scroll) tbody tr:hover > td{background:transparent}
  /* The card view stacks cells, so the fixed row height and the column rules
     from the desktop grid have to be undone here or every cell is 32px tall
     with a stray line down its side. */
  .tp-tbl-wrap:not(.tp-tbl-scroll) tbody td{
    display:flex;align-items:baseline;gap:12px;
    height:auto;min-height:0;
    padding:7px 13px;text-align:start;
    border-right:0;border-left:0;border-bottom:1px solid #f2f5f9;
  }
  .tp-tbl-wrap:not(.tp-tbl-scroll) tbody tr.tp-selected{box-shadow:inset 3px 0 0 var(--tp-primary)}
  .tp-tbl-wrap:not(.tp-tbl-scroll) tbody tr td:last-child{border-bottom:0}
  .tp-tbl-wrap:not(.tp-tbl-scroll) tbody td::before{
    content:attr(data-label);flex:0 0 40%;
    font-size:11.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
    color:var(--tp-muted);
  }
  .tp-tbl-wrap:not(.tp-tbl-scroll) tbody td:first-child{
    background:var(--tp-surface);font-weight:700;
    border-start-start-radius:var(--tp-r);border-start-end-radius:var(--tp-r);
  }
  .tp-tbl-wrap:not(.tp-tbl-scroll) tbody td.tp-num{justify-content:space-between}

  /* Actions collapse into one row of buttons at the foot of the card rather
     than three separate labelled lines. */
  /* A card has nothing to scroll sideways, so the pinning is undone here —
     a sticky cell inside a stacked card floats away from its own row. */
  /* A card has nothing to scroll and no columns to divide, so both the
     pinning and the column rules come off. */
  .tp-tbl-wrap:not(.tp-tbl-scroll) tbody td.tp-act,
  .tp-tbl-wrap:not(.tp-tbl-scroll) thead th.tp-act{
    position:static;inset-inline-end:auto;background:transparent;
  }
  .tp-tbl-wrap:not(.tp-tbl-scroll) tbody td.tp-act,
  .tp-tbl-wrap:not(.tp-tbl-scroll) tbody td.tp-act-first{
    border-inline:0;
  }
  .tp-tbl-wrap:not(.tp-tbl-scroll) tbody td.tp-act{
    display:inline-flex;width:auto;padding:8px 6px 8px 13px;border-bottom:0;
  }
  .tp-tbl-wrap:not(.tp-tbl-scroll) tbody td.tp-act::before{display:none}
  .tp-tbl-wrap:not(.tp-tbl-scroll) tbody td.tp-act .tp-act-btn{
    width:38px;height:38px;border-color:var(--tp-line);
  }

  .tp-linegrid{min-width:760px}
  .tp-pager-nav{margin-inline-start:0;width:100%}
  .tp-report-tools{margin-inline-start:0;width:100%}
  .tp-report-tools .tp-btn{flex:1}
  .tp-viewer{height:70vh}
}
@media (max-width:560px){
  .tp-kpi-grid{grid-template-columns:1fr}
  .tp-kpi-val{font-size:20px}
  .tp-login-wrap{padding:22px 13px 28px}
  .tp-login-card{padding:26px 20px 22px;border-radius:12px}
  .tp-signout .tp-btn-text{display:none}
  .tp-signout{padding:0 11px}
}

/* ---------- 18. RTL ------------------------------------------------------ */
html[dir="rtl"] body,body[dir="rtl"]{font-family:var(--tp-font-ar)}
html[dir="rtl"] .tp-right,body[dir="rtl"] .tp-right{text-align:start}
body[dir="rtl"] .tp-menu-caret{transform:rotate(180deg)}
body[dir="rtl"] .tp-menu-group.tp-open>.tp-menu-link .tp-menu-caret{transform:rotate(-90deg)}
body[dir="rtl"] .tp-modal-foot{justify-content:flex-start}
body[dir="rtl"] .tp-tbl tbody tr.tp-selected{box-shadow:inset -3px 0 0 var(--tp-primary)}

/* ---------- 19. PRINT ---------------------------------------------------- */
@media print{
  @page{size:A4;margin:12mm 10mm}
  body{background:#fff;color:#000;font:11pt/1.35 "Segoe UI",Arial,sans-serif}

  .tp-topbar,.tp-sidebar,.tp-backdrop,.tp-tabbar,.tp-cmdbar,.tp-pager,
  .tp-toasts,.tp-filter,.tp-page-head,.tp-noprint{display:none!important}

  /* position must be reset too, or an absolutely positioned page prints
     as a single clipped screen instead of flowing across pages. */
  body.tp-framed{position:static!important;overflow:visible!important}
  .tp-page-host,
  .tp-shell,.tp-main,.tp-page,.tp-page-body,.tp-workspace{
    position:static!important;
    display:block!important;height:auto!important;overflow:visible!important;
    margin:0!important;padding:0!important;
  }
  .tp-card{border:0!important;box-shadow:none!important;border-radius:0!important;margin:0!important;max-width:none!important}
  .tp-card-head,.tp-card-body{padding:0!important;border:0!important}

  .tp-print-head{
    display:flex;align-items:flex-start;gap:14px;
    border-bottom:1.5pt solid #000;padding-bottom:6pt;margin-bottom:10pt;
  }
  .tp-print-logo{height:52px;width:auto}
  .tp-print-co{font-size:14pt;font-weight:700;line-height:1.2}
  .tp-print-co-sub{font-size:9pt;color:#333}
  .tp-print-title{
    margin-inline-start:auto;text-align:end;font-size:13pt;font-weight:700;
    text-transform:uppercase;letter-spacing:.04em;
  }
  .tp-print-meta{display:grid;grid-template-columns:1fr 1fr;gap:2pt 18pt;margin-bottom:9pt;font-size:10pt}
  .tp-print-meta span{display:inline-block;min-width:78pt;font-weight:700}

  .tp-tbl,.tp-linegrid{width:100%!important;min-width:0!important;border-collapse:collapse;font-size:9.5pt}
  .tp-tbl thead,.tp-linegrid thead{display:table-header-group}
  .tp-tbl tfoot,.tp-linegrid tfoot{display:table-footer-group}
  .tp-tbl tr,.tp-linegrid tr{page-break-inside:avoid}
  .tp-tbl th,.tp-tbl td,.tp-linegrid th,.tp-linegrid td{
    border:.5pt solid #444!important;padding:3pt 5pt!important;
    position:static!important;white-space:normal!important;
    background:#fff!important;color:#000!important;
  }
  .tp-tbl thead th,.tp-linegrid thead th,
  .tp-tbl tfoot td,.tp-linegrid tfoot td{background:#eef2f7!important;font-weight:700}
  .tp-tbl .tp-num,.tp-linegrid .tp-amt{text-align:right!important}
  .tp-tbl-wrap,.tp-linegrid-wrap{overflow:visible!important;border:0!important}

  /* the mobile card transform must never reach paper */
  .tp-tbl,.tp-tbl thead,.tp-tbl tbody,.tp-tbl tfoot,.tp-tbl tr{display:revert!important}
  .tp-tbl th,.tp-tbl td{display:table-cell!important}
  .tp-tbl tbody td::before,.tp-tbl tfoot td::before{content:none!important}

  input,select,textarea{
    border:0!important;box-shadow:none!important;background:transparent!important;
    color:#000!important;padding:0!important;
  }

  .tp-print-foot{
    margin-top:12pt;padding-top:5pt;border-top:.5pt solid #666;
    font-size:8.5pt;color:#333;display:flex;justify-content:space-between;
  }
  .tp-print-sign{margin-top:26pt;display:flex;justify-content:space-between;font-size:9.5pt}
  .tp-print-sign div{border-top:.5pt solid #000;padding-top:3pt;min-width:150pt;text-align:center}
  .tp-break{page-break-before:always}

  /* ---------- attendance sheet ----------
     Sized so ONE employee — masthead, header, 31 day rows, totals and
     signatures — lands on ONE A4 page. The budget, at 12mm/10mm margins:

       usable                        774pt
       masthead + meta                48pt
       employee header (3 rows)       34pt
       table head                     14pt
       31 rows at 13.5pt             418pt
       totals + signatures            58pt
                                   -------
                                     572pt

     That leaves room, so the type stays readable rather than being
     shrunk to fit a page it already fits. */
  .tp-printonly{display:block!important}

  /* An employee block is a unit: the masthead, the header naming them,
     their days and their totals. Split across a page break, the totals
     line would sit under days it does not cover. */
  .tp-sheetblock{page-break-inside:avoid;margin-bottom:0}

  /* One employee per page. An attendance sheet is signed per person, and
     two people on one sheet cannot be signed separately. The masthead is
     inside the block, so every page carries it. */
  .tp-sheetblock + .tp-sheetblock{page-break-before:always}

  /* Row height is what decides whether 31 days fit. Fixed here rather
     than left to the default line-height, which changes with the browser
     and would push the 31st row onto a second page on some machines. */
  .tp-sheetblock .tp-tbl{font-size:9pt;line-height:1.15}
  .tp-sheetblock .tp-tbl th,
  .tp-sheetblock .tp-tbl td{padding:2pt 4pt!important;height:13.5pt}
  .tp-sheetblock .tp-tbl thead th{font-size:8.5pt;letter-spacing:.02em}

  /* The masthead, compact: it repeats on every page and must not eat the
     space the rows need. */
  .tp-sheetblock .tp-print-head{padding-bottom:4pt;margin-bottom:5pt}
  .tp-sheetblock .tp-print-co{font-size:12.5pt}
  .tp-sheetblock .tp-print-co-sub{font-size:8.5pt}
  .tp-sheetblock .tp-print-title{font-size:11pt}
  .tp-sheetblock .tp-print-meta{
    grid-template-columns:1fr 1fr;gap:1pt 18pt;margin-bottom:6pt;font-size:8.5pt
  }
  .tp-sheetblock .tp-print-meta span{min-width:52pt}
  .tp-sheetblock .tp-print-sign{margin-top:14pt;font-size:9pt}

  .tp-sheethead{
    display:grid!important;grid-template-columns:1fr 1fr;gap:0 18pt;
    border-bottom:.5pt solid #000;padding-bottom:4pt;margin-bottom:5pt;font-size:9pt
  }
  .tp-sheetpair{display:flex;gap:6pt;line-height:1.3}
  .tp-sheetpair .tp-lbl{min-width:62pt;font-weight:700;margin:0;font-size:9pt}

  /* The badges are screen furniture: on paper the code reads better as
     plain text than as a grey pill. */
  .tp-badge{
    background:transparent!important;border:0!important;padding:0!important;
    color:#000!important;font-weight:600
  }

  /* A rest day stays visibly different without wasting toner. */
  .tp-row-rest > td{background:#f4f4f4!important;color:#555!important}
  .tp-row-warn > td{background:#fff!important;font-weight:700}

  /* The totals line under each employee. */
  .tp-ovr-note{
    border:0!important;border-top:.5pt solid #666!important;
    padding:4pt 0 0!important;margin-top:4pt!important;background:transparent!important
  }
  .tp-ovr-note-t{font-size:10pt;font-weight:700}

  /* Room to sign, on every sheet, because that is what it is for. */
  .tp-sheetblock .tp-tbl-wrap::after{content:""}
}

/* ======================================================================
   FORM TABS IN THE PAGE HEAD

   The List / Entry tabs sit in the header rather than in a strip of their
   own. Three stacked bars — pinned tabs, page head, form tabs — spent most
   of the top of the screen saying where you are.

   The tabs are MOVED here by script, so the rules below have to work
   wherever they land: on a page whose script has not run yet they are
   still inside the body and must not look broken.
   ====================================================================== */

.tp-head-tabs{display:flex;align-items:center;flex:0 0 auto;margin-inline-end:14px}
.tp-head-tabs:empty{display:none;margin:0}

/* A segmented control: one outlined group, the active half filled. Reads
   as a choice between two things rather than two separate buttons. */
.tp-formtabs{
  display:inline-flex;align-items:stretch;gap:0;
  border:1px solid var(--tp-line-strong);border-radius:10px;
  background:var(--tp-surface);overflow:hidden;
  flex:0 0 auto;white-space:nowrap
}

/* Every property the earlier rule sets is overridden here, not just the
   ones being changed: it draws a 3px transparent border, a radius and a
   box-shadow ring, and leaving any of those in place fights the segmented
   outline this replaces. */
.tp-formtab{
  display:inline-flex;align-items:center;gap:7px;
  min-height:0;padding:7px 16px;
  border:0;border-radius:0;box-shadow:none;
  background:transparent;cursor:pointer;
  font-family:var(--tp-font);font-size:13px;font-weight:600;line-height:1;
  color:var(--tp-muted);
  transition:background var(--tp-t),color var(--tp-t)
}

/* A rule between the halves, not around each: the group is one control. */
.tp-formtab + .tp-formtab{border-inline-start:1px solid var(--tp-line)}

.tp-formtab:hover{background:var(--tp-surface-2);color:var(--tp-ink)}

/* Filled, no ring: the group already has one outline and a second inside it
   reads as two controls. */
.tp-formtab.tp-active{
  background:var(--tp-primary-soft);color:var(--tp-primary);
  border-color:transparent;box-shadow:none;font-weight:600
}

/* The count sits inside the tab it belongs to. */
.tp-formtab-count{
  font-weight:700;font-size:11px;opacity:.75;
  padding:1px 6px;border-radius:20px;background:rgba(0,0,0,.06)
}
.tp-formtab.tp-active .tp-formtab-count{background:rgba(255,255,255,.55)}
.tp-formtab-count:empty{display:none}

/* The head is now carrying tabs, a title, a clock and a format switch, so
   it wraps rather than squeezing any of them. */
.tp-page-head{flex-wrap:wrap;row-gap:8px}

@media (max-width:720px){
  /* Narrow: the tabs go full width on their own line rather than
     shrinking to unreadable. Still not scrollable. */
  .tp-head-tabs{order:3;flex:1 0 100%;margin-inline-end:0}
  .tp-formtabs{width:100%}
  .tp-formtab{flex:1 1 0;justify-content:center}
}

/* ======================================================================
   PAGE HEAD: COMPACT, AND EVERYTHING ON ONE MIDDLE LINE

   Three faults were making it tall and ragged:

     MIXED ALIGNMENT. .tp-page-titles and .tp-clock-text aligned on
     BASELINE while the head itself aligned on CENTER. A baseline box
     inside a centred row sits wherever its own text sits, so the title,
     the clock and the tabs each found a different line.

     STACKED PADDING. The head padded 7px, the tabs padded 7px inside a
     border, and the clock padded 3px inside its own pill. Three paddings
     add up even though only the tallest matters.

     THE INACTIVE TAB WAS --tp-muted, which is the colour of a disabled
     control. "List" is not disabled; it is the half you are not on.

   Everything below is an override of an earlier rule, so each property
   that earlier rule sets is repeated rather than assumed.
   ====================================================================== */

/* One row, one alignment. Nothing in the head aligns on baseline. */
.tp-page-head{
  padding:5px 16px;gap:12px;
  align-items:center;
  min-height:44px
}

.tp-page-titles{align-items:center;line-height:1.15}
.tp-page-title{font-size:16px;line-height:1.15}

.tp-head-right{align-items:center;gap:10px}

/* The clock pill loses its own vertical padding: the row sets the height,
   and the pill only needs to be wide enough to read. */
.tp-clock{padding:2px 11px 2px 8px;align-items:center}
.tp-clock-text{align-items:center;gap:7px;line-height:1.15}
.tp-clock-time{align-items:center;font-size:14px;line-height:1.15}
.tp-clock-date{font-size:11px;line-height:1.15}

/* The tabs match the clock's height rather than setting their own. */
.tp-head-tabs{align-items:center;margin-inline-end:4px}

/* margin:0 is the point of this rule, not the radius.
   The original .tp-formtabs carries margin-bottom:18px — correct when the
   tabs were their own strip above the form, and wrong now they are a flex
   item in a centred row: the browser centres the BOX INCLUDING its margin,
   so 18px underneath lifts the visible tabs 9px above everything else.
   Every override so far changed appearance and left the margin alone. */
.tp-formtabs{margin:0;border-radius:8px;align-self:center}

.tp-formtab{
  padding:5px 14px;min-height:28px;
  align-items:center;line-height:1.15;
  font-size:13px;font-weight:600;

  /* Readable, not disabled. The active half is what carries the emphasis;
     the inactive one still has to be legible enough to click on purpose. */
  color:var(--tp-ink)
}

.tp-formtab:hover{background:var(--tp-surface-2);color:var(--tp-ink)}

.tp-formtab.tp-active{
  background:var(--tp-primary-soft);color:var(--tp-primary);
  border-color:transparent;box-shadow:none
}

.tp-formtab-count{font-size:11px;padding:0 5px;line-height:1.4}

/* ======================================================================
   ATTENDANCE CALENDAR

   A month of one employee as tiles. Colour carries the state, and every
   colour is paired with text inside the tile: a legend alone fails for
   anybody who cannot separate the greens from the greys, and a payroll
   query is exactly when that matters.
   ====================================================================== */

.tp-calblock{background:var(--tp-surface);border:1px solid var(--tp-line);
  border-radius:var(--tp-r);padding:14px 16px;margin-bottom:14px}

.tp-calhead{display:flex;align-items:center;flex-wrap:wrap;gap:10px;
  padding-bottom:10px;margin-bottom:12px;border-bottom:1px solid var(--tp-line)}
.tp-calhead-who{font-size:15px}
.tp-calhead-when{margin-inline-start:auto;font-size:12px}

.tp-calstats{display:grid;gap:8px;margin-bottom:14px;
  grid-template-columns:repeat(auto-fit,minmax(96px,1fr))}
.tp-calstat{background:var(--tp-surface-2);border-radius:var(--tp-r);padding:8px 10px}
.tp-calstat-l{font-size:11px;color:var(--tp-muted)}
.tp-calstat-v{font-size:20px;font-weight:700;line-height:1.25}
.tp-calstat-n{font-size:11px;color:var(--tp-muted)}

.tp-calgrid{display:grid;grid-template-columns:repeat(7,1fr);gap:4px}
.tp-caldow{font-size:11px;color:var(--tp-muted);text-align:center;padding-bottom:2px}

.tp-calcell{min-height:64px;border:1px solid var(--tp-line);border-radius:6px;
  padding:4px 6px;background:var(--tp-surface)}
.tp-calcell-out{background:transparent;border-color:transparent}

.tp-caldate{font-size:11px;font-weight:700;line-height:1.3}
.tp-caltimes{font-size:10px;line-height:1.4;font-variant-numeric:tabular-nums}
.tp-caltag{font-size:10px;line-height:1.4}
.tp-callate{font-size:10px;font-weight:700;line-height:1.4}

/* One pair per state. The text colour is the darkest stop of the same
   family, never plain grey on a tint. */
.tp-cal-s1{background:#E1F5EE;border-color:#0F6E56}
.tp-cal-s1 .tp-caldate,.tp-cal-s1 .tp-caltimes,.tp-cal-s1 .tp-caltag{color:#04342C}

.tp-cal-s2{background:#FAEEDA;border-color:#854F0B}
.tp-cal-s2 .tp-caldate,.tp-cal-s2 .tp-caltimes,.tp-cal-s2 .tp-caltag,
.tp-cal-s2 .tp-callate{color:#412402}

.tp-cal-s3{background:#FCEBEB;border-color:#A32D2D}
.tp-cal-s3 .tp-caldate,.tp-cal-s3 .tp-caltag{color:#501313}

.tp-cal-s4{background:#E6F1FB;border-color:#185FA5}
.tp-cal-s4 .tp-caldate,.tp-cal-s4 .tp-caltag{color:#042C53}

.tp-cal-s5{background:#F1EFE8;border-color:#B4B2A9}
.tp-cal-s5 .tp-caldate,.tp-cal-s5 .tp-caltag{color:#2C2C2A}

/* Missing punch is dashed: it is an incomplete record, not a state
   somebody chose, and the border says so before the colour does. */
.tp-cal-s6{background:var(--tp-surface);border-style:dashed;border-color:#EF9F27}
.tp-cal-s6 .tp-caldate,.tp-cal-s6 .tp-caltimes{color:#854F0B}

.tp-callegend{display:flex;flex-wrap:wrap;gap:14px;margin-top:12px;
  padding-top:10px;border-top:1px solid var(--tp-line);font-size:12px}
.tp-callegend-i{display:inline-flex;align-items:center;gap:6px}
.tp-calswatch{width:11px;height:11px;border-radius:3px;border:1px solid}

@media print{
  .tp-noprint{display:none !important}
  .tp-calblock{border:0;padding:0}
  /* Colour is the whole point of this page, so it must survive the
     printer's default of stripping backgrounds. */
  .tp-calcell{-webkit-print-color-adjust:exact;print-color-adjust:exact}
  .tp-calstat{-webkit-print-color-adjust:exact;print-color-adjust:exact}
}
