<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

	<xsl:output method="html" version="5.0"/>

	<!-- ============================================================
	     ClientSpace report widget - "My Cases" (stored procedure: peo_xslt_case)
	     Anchors: Workspace Landing page, Client Service Case page
	     SP filters: @CurrentWorkspace, @CurrentUser, @CurrentRecord
	                 (these are the names XSLTQueryController passes; the proc
	                  skips any filter it receives as 0)

	     EXPECTED COLUMN CONTRACT (element name = SP column alias).
	     Return the Client group first, then the Case group:

	       pkClientMasterID   (hidden - drives the Client Master link)
	       pkCaseID           (hidden - the record id in the Case # link)
	       CaseProjTypeID     (hidden - the dataform project type the Case #
	                           link navigates to. Falls back to
	                           $caseProjTypeFallback when absent.)
	       fkProjectID        (hidden, and OPTIONAL - the links no longer use
	                           it. The dataform edit route derives the
	                           workspace from the record itself; see the URL
	                           notes below. Listed in $hiddenCols only so that
	                           a proc which still selects it does not sprout a
	                           stray "fkProjectID" column in the grid.)
	       ClientNumber       \
	       ImportID            |
	       LegalName           |  <- LegalName is tblOrganization.Name.
	       ClientStatus        |     These seven never render as table columns;
	       ContractType        |     they are collapsed into the client context
	       ContractEffectiveDate|    card above the KPI strip. See $clientCols.
	       DateOf1stPayroll   /
	       CaseNumber
	       CaseStatus             <- aliased so it can't collide with ClientStatus
	       Priority
	       Category               (decoded)
	       CaseType               (decoded)
	       Subject
	       fkUserIDAssignedTo (hidden - the raw assigned-to user id. Never
	                           rendered as a column; it is copied onto the
	                           row as data-assigneduserid so the My Cases
	                           button can filter to the signed-in user.)
	       AssignedTo             'FirstName LastName'
	       Employee               'FirstName LastName (EEIdentifier)'
	       DueDate
	       CreateDate
	       CreateTime
	       ResolutionDate     (hidden by request - accepted, never rendered)
	       ResolutionTime     (hidden by request - accepted, never rendered)
	       Issue
	       Description
	       Resolution

	     Any column not listed still renders - it just falls through to
	     the generic label/cell styling instead of a chip or link.
	     ============================================================ -->

	<!-- Cases open the Client Service Case dataform through
	     csApp.navigation.go. Three things matter here:
	       * The URL form is the app-standard dataform EDIT route,
	         '~/dataform/edit/{ProjectTypeID}/{GenID}' (RouteConfig
	         "DataformEdit"). This is the same URL the app itself uses
	         everywhere it opens an existing record: DataformSearchViewModel
	         .EditUrl, the dataform's own ReloadUrl in DataformController,
	         Users/WatchWidget.js, tasks/ActionCenter.js,
	         Files/DocumentSearch.js and SurveyLinkController.
	         Do NOT add a ProjectID to it, and do not switch to the
	         'Dataform?ProjectID=&GenID=&ProjectTypeID=' query-string form:
	           - ProjectID is only needed to CREATE a record, which has no
	             workspace of its own yet (hence ~/dataform/create/{pt}/{proj}).
	             For an existing record DataformController reads the workspace
	             off the loaded record - Dataform.ProjectID is mapped from the
	             record XML, not from the request - so the non-admin
	             HasProjectAccess check already gets the real project id.
	           - The bare query-string form is not served at all: IIS answers
	             /Next/Dataform with "This type of page is not served" before
	             MVC routing runs.
	         Note the GenID must be the LAST segment emitted - the route's
	         next slot is {ParentFieldName}, so a stray extra '/' silently
	         shifts the record id into it and the dataform opens blank.
	       * The URL never goes in the href. It is rendered into the anchor's
	         data-url attribute and wireRecordLinks() (in the script at the
	         bottom) hands it to csApp.navigation.go on click. A javascript:
	         href is itself a URL, so any layer that re-encodes it escapes the
	         quotes around the inner URL and the browser navigates to an
	         address with a trailing %27. data-url is not a URI attribute and
	         nothing touches it. This also removes the old target="_top"
	         caveat: there is no javascript: URI left to be swallowed.
	       * $caseRouteBase uses the portable tilde form ('~/dataform/edit')
	         rather than an app-root-relative path ('/Next/dataform/edit',
	         which assumes the app is served from /Next) - csApp.server.formatUrl
	         resolves a leading '~/' to whatever root the instance runs under.
	     The project type id comes from CaseProjTypeID, which the proc resolves
	     from ProjectTypes because that id differs per environment.
	     $caseProjTypeFallback only applies if a proc stops returning it. -->
	<xsl:variable name="caseRouteBase">~/dataform/edit</xsl:variable>
	<xsl:variable name="clientRoute">~/dataform/edit</xsl:variable>
	<!-- Only applies if a proc stops returning CaseProjTypeID. The Client
	     Master project type has no equivalent column, so it stays a constant
	     here - check it if this widget moves to another environment. -->
	<xsl:variable name="caseProjTypeFallback">246</xsl:variable>
	<xsl:variable name="clientProjType">196</xsl:variable>

	<!-- XSLT 1.0 has no lower-case(); translate() stands in for it -->
	<xsl:variable name="uc">ABCDEFGHIJKLMNOPQRSTUVWXYZ</xsl:variable>
	<xsl:variable name="lc">abcdefghijklmnopqrstuvwxyz</xsl:variable>

	<!-- Client-level columns. These are always collapsed into the client
	     context card and never rendered as Case table columns, so adding a
	     name here moves that column out of the table and into the card. -->
	<xsl:variable name="clientCols">|pkClientMasterID|ClientNumber|ImportID|LegalName|Name|ClientStatus|ContractType|ContractEffectiveDate|DateOf1stPayroll|FirstPayrollDate|</xsl:variable>
	<!-- Key/plumbing columns: rendered as display:none cells, never as data.
	     ResolutionDate/ResolutionTime are here by request - the proc still
	     returns them, they just don't show (and the CSV export skips
	     display:none cells, so they stay out of the export too). -->
	<xsl:variable name="hiddenCols">|pkClientMasterID|pkCaseID|CaseProjTypeID|fkProjectID|fkUserIDAssignedTo|ResolutionDate|ResolutionTime|</xsl:variable>
	<xsl:variable name="greenStatuses">|closed|resolved|complete|completed|done|</xsl:variable>
	<xsl:variable name="amberStatuses">|open|in progress|in process|working|active|assigned|</xsl:variable>
	<xsl:variable name="blueStatuses">|new|submitted|</xsl:variable>
	<!-- Waiting-on-something statuses: the On Hold KPI tile and board
	     column. Keep in sync with HOLD_STATUSES in the script below. -->
	<xsl:variable name="holdStatuses">|on hold|hold|deferred|pending|waiting|suspended|</xsl:variable>
	<xsl:variable name="redStatuses">|escalated|reopened|overdue|</xsl:variable>
	<xsl:variable name="notOpenStatuses">|closed|resolved|complete|completed|done|cancelled|canceled|void|</xsl:variable>
	<xsl:variable name="clientGreen">|active|current|</xsl:variable>
	<xsl:variable name="clientBlue">|pending|prospect|implementation|onboarding|new|</xsl:variable>
	<xsl:variable name="clientGray">|terminated|inactive|closed|former|</xsl:variable>
	<xsl:variable name="hotPriorities">|high|critical|urgent|emergency|1|1 - high|p1|</xsl:variable>
	<xsl:variable name="midPriorities">|medium|normal|standard|moderate|2|2 - medium|p2|</xsl:variable>
	<xsl:variable name="lowPriorities">|low|minor|3|3 - low|p3|</xsl:variable>


	<xsl:template match="/">
		<html>
			<head>
				<!-- Inter everywhere. The font stacks carry system fallbacks,
				     so if the host can't reach Google Fonts the widget
				     degrades gracefully. -->
				<!-- Without this, phones render the widget at desktop width
				     zoomed out to fit. -->
				<meta name="viewport" content="width=device-width, initial-scale=1"/>
				<link rel="preconnect" href="https://fonts.googleapis.com"/>
				<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous"/>
				<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&amp;display=swap"/>
				<style>
					/* ==========================================================
					Prismatic design tokens (brandColors from the PrismHR
					design system). Declared on the widget root rather than
					:root so nothing leaks into the host ClientSpace page.
					========================================================== */
					.cs-specific-widget {
					--white:#ffffff;
					/* Blue ramp - Prismatic's primary (blue500 #0f5cf5) is the
					   same accent ClientSpace's own chrome uses, so the widget
					   toolbar, links, and sort highlight all key off it. */
					--blue100:#e5f8ff; --blue200:#bfe4ff; --blue300:#a8d9ff;
					--blue500:#0f5cf5; --blue600:#0048b0; --blue700:#003080;
					--green100:#e3fcef; --green200:#abf5d1; --green600:#00875a; --green800:#006644;
					--yellow100:#fffae6; --yellow200:#fff0b3; --yellow300:#ffe380; --yellow600:#ff991f;
					--orange100:#ffe4cf; --orange200:#ffc9a0; --orange600:#ff6f00; --orange700:#99480a;
					--red100:#ffebe6; --red200:#ffbdad; --red600:#de350b; --red700:#bf2600;
					--purple100:#f0e5ff; --purple200:#d7bfff; --purple500:#450ff5; --purple700:#250080;
					--gray100:#f9f9f9; --gray150:#f6f6f6; --gray200:#f3f3f4;
					--gray300:#dcdee0; --gray450:#8c949a; --gray500:#697077; --gray600:#2a3742;

					--primary: var(--blue500);
					--primary-ghost: var(--blue100);
					--link-color: var(--blue600);
					--card-border-color: var(--gray300);
					--card-border-radius: 6px;
					--base-margin: 8px;
					--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
					/* Dates/IDs use Inter too - tabular-nums on the cell
					   classes keeps the digits aligned without a mono face. */
					--mono: var(--font-family);

					font-family: var(--font-family);
					font-size: 12px;
					color: var(--gray600);
					-webkit-font-smoothing: antialiased;
					}
					.cs-specific-widget * { box-sizing: border-box; }

					/* ---------- Dark mode ----------
					   Every color in the widget flows through the custom
					   properties above, so dark mode is one override block:
					   surfaces invert, ramps flip to dark tints with brighter
					   accents. The blue toolbar stays blue in both modes. */
					.cs-specific-widget.cs-dark {
					--white:#1e2633;
					--blue100:#16324a; --blue200:#1e4a70; --blue300:#265d8c;
					--blue500:#3b82f6; --blue600:#7ab3ff; --blue700:#a8ccff;
					--green100:#0f3325; --green200:#14532d; --green600:#4ade80; --green800:#86efac;
					--yellow100:#3a2e10; --yellow200:#584410; --yellow300:#6b5316; --yellow600:#fbbf24;
					--orange100:#40260f; --orange200:#5c3410; --orange600:#fb923c; --orange700:#fdba74;
					--red100:#431418; --red200:#5f1a1f; --red600:#f87171; --red700:#fca5a5;
					--purple100:#2b1d4d; --purple200:#3c2a69; --purple500:#a78bfa; --purple700:#c4b5fd;
					--gray100:#141a24; --gray150:#1a212e; --gray200:#242e3d;
					--gray300:#334052; --gray450:#8395ab; --gray500:#9fb0c5; --gray600:#dbe4f0;
					}
					/* Spots where --white is used as TEXT on a blue fill must
					   stay genuinely white in dark mode (the fills stay blue). */
					.cs-specific-widget.cs-dark .cs-toolbar-title { color: #ffffff; }
					.cs-specific-widget.cs-dark .bt { color: #ffffff; }
					.cs-specific-widget.cs-dark .cs-card-nav { color: #ffffff; }
					.cs-specific-widget.cs-dark .cs-cardlist-item.cs-cardlist-active .cs-cardlist-num,
					.cs-specific-widget.cs-dark .cs-cardlist-item.cs-cardlist-active .cs-cardlist-subj { color: #ffffff; }
					.cs-specific-widget.cs-dark .cs-cardlist-item.cs-cardlist-active .cs-cardlist-dot { background: #ffffff; }

					/* ---------- Client context card ---------- */
					.cs-specific-widget .cs-clientcard {
					border: 1px solid var(--card-border-color);
					border-radius: var(--card-border-radius);
					background: var(--white);
					padding: 10px 12px;
					margin-bottom: var(--base-margin);
					}
					.cs-specific-widget .cs-cc-head {
					display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
					padding-bottom: 8px; margin-bottom: 8px;
					border-bottom: 1px solid var(--gray200);
					}
					.cs-specific-widget .cs-cc-name {
					font-size: 15px; font-weight: 600; letter-spacing: -0.01em;
					color: var(--gray600); margin-right: 2px;
					}
					.cs-specific-widget .cs-cc-name a { color: var(--link-color); text-decoration: none; }
					.cs-specific-widget .cs-cc-name a:hover { text-decoration: underline; }
					.cs-specific-widget .cs-cc-grid {
					display: flex; flex-wrap: wrap; gap: 4px 28px;
					}
					.cs-specific-widget .cs-cc-field { display: flex; flex-direction: column; gap: 1px; }
					.cs-specific-widget .cs-cc-lbl {
					font-size: 10px; font-weight: 600; text-transform: uppercase;
					letter-spacing: 0.06em; color: var(--gray450);
					}
					.cs-specific-widget .cs-cc-val { font-size: 12px; color: var(--gray600); }
					.cs-specific-widget .cs-cc-val.cs-num { font-family: var(--mono); }

					/* ---------- KPI strip ---------- */
					.cs-specific-widget .cs-kpis {
					display: flex; flex-wrap: wrap; gap: var(--base-margin);
					margin-bottom: var(--base-margin);
					}
					/* Soft pastel gradient tiles, matching the reference
					   screenshot: big dark number with the colored icon right
					   beside it, sentence-case label underneath, gradient wash
					   per KPI. All colors stay brand-palette tints. */
					.cs-specific-widget .cs-kpi {
					flex: 1 1 110px; min-width: 110px;
					border: none;
					border-radius: 10px;
					padding: 10px 12px;
					}
					.cs-specific-widget .cs-kpi-top {
					display: flex; align-items: center; gap: 8px;
					}
					.cs-specific-widget .cs-kpi-ico { flex: none; display: block; }
					.cs-specific-widget .cs-kpi-num {
					font-size: 22px; font-weight: 700; line-height: 1.1;
					font-variant-numeric: tabular-nums; color: var(--gray600);
					letter-spacing: -0.02em;
					}
					.cs-specific-widget .cs-kpi-lbl {
					font-size: 11px; font-weight: 500;
					color: var(--gray600); margin-top: 3px;
					}
					/* Tiles double as filters. Hover = pizazz: the tile lifts
					   and scales, glows in its own color, the icon pops with a
					   tilt, the number swells, and a shine sweeps across the
					   gradient. The pressed filter keeps its blue ring. */
					.cs-specific-widget .cs-kpi {
					cursor: pointer; user-select: none;
					position: relative; overflow: hidden;
					transition: transform 0.15s ease, box-shadow 0.15s ease, filter 0.15s ease;
					}
					.cs-specific-widget .cs-kpi:hover {
					transform: translateY(-3px) scale(1.03);
					filter: saturate(1.25);
					}
					.cs-specific-widget .cs-kpi::after {
					content: ""; position: absolute; top: 0; left: -80%;
					width: 50%; height: 100%;
					background: linear-gradient(100deg, rgba(255,255,255,0) 20%, rgba(255,255,255,0.6) 50%, rgba(255,255,255,0) 80%);
					transform: skewX(-20deg);
					opacity: 0; pointer-events: none;
					}
					.cs-specific-widget .cs-kpi:hover::after { animation: csKpiShine 0.65s ease; }
					@keyframes csKpiShine {
					0% { left: -80%; opacity: 1; }
					100% { left: 130%; opacity: 1; }
					}
					.cs-specific-widget .cs-kpi-ico { transition: transform 0.18s ease; }
					/* The icon wakes up on hover: a quick bouncy wiggle, then
					   it stays enlarged while the pointer is on the tile. */
					.cs-specific-widget .cs-kpi:hover .cs-kpi-ico {
					transform: scale(1.3);
					animation: csIcoWiggle 0.55s ease;
					}
					@keyframes csIcoWiggle {
					0%   { transform: scale(1) rotate(0deg); }
					25%  { transform: scale(1.45) rotate(-14deg); }
					50%  { transform: scale(1.3) rotate(11deg); }
					75%  { transform: scale(1.38) rotate(-7deg); }
					100% { transform: scale(1.3) rotate(0deg); }
					}
					.cs-specific-widget .cs-kpi-num { transition: transform 0.18s ease; transform-origin: left center; display: inline-block; }
					.cs-specific-widget .cs-kpi:hover .cs-kpi-num { transform: scale(1.08); }
					/* Per-tile colored glow on hover. */
					.cs-specific-widget .cs-kpi-total:hover { box-shadow: 0 8px 18px var(--gray300); }
					.cs-specific-widget .cs-kpi-new:hover   { box-shadow: 0 8px 18px var(--blue200); }
					.cs-specific-widget .cs-kpi-prog:hover  { box-shadow: 0 8px 18px var(--yellow200); }
					.cs-specific-widget .cs-kpi-hold:hover  { box-shadow: 0 8px 18px var(--purple200); }
					.cs-specific-widget .cs-kpi-soon:hover  { box-shadow: 0 8px 18px var(--green200); }
					.cs-specific-widget .cs-kpi-late:hover  { box-shadow: 0 8px 18px var(--orange200); }
					.cs-specific-widget .cs-kpi-active { box-shadow: inset 0 0 0 2px var(--blue500); }
					.cs-specific-widget .cs-kpi.cs-kpi-active:hover { box-shadow: inset 0 0 0 2px var(--blue500), 0 8px 18px var(--blue200); }
					.cs-specific-widget tbody tr.cs-kpi-hidden { display: none !important; }
					.cs-specific-widget .cs-kpi-total { background: linear-gradient(165deg, var(--gray100) 20%, var(--gray200) 100%); }
					.cs-specific-widget .cs-kpi-new  { background: linear-gradient(165deg, var(--blue100) 20%, var(--blue200) 100%); }
					.cs-specific-widget .cs-kpi-prog { background: linear-gradient(165deg, var(--yellow100) 20%, var(--yellow200) 100%); }
					.cs-specific-widget .cs-kpi-hold { background: linear-gradient(165deg, var(--purple100) 20%, var(--purple200) 100%); }
					.cs-specific-widget .cs-kpi-soon { background: linear-gradient(165deg, var(--green100) 20%, var(--green200) 100%); }
					.cs-specific-widget .cs-kpi-late { background: linear-gradient(165deg, var(--orange100) 20%, var(--orange200) 100%); }

					/* ---------- Table shell ---------- */
					.cs-specific-widget .cs-tablewrap {
					border: 1px solid var(--card-border-color);
					border-radius: 10px;
					background: var(--white);
					overflow: auto;
					/* no-JS fallback only: the sizing script below replaces
					   this with the space actually available in the widget
					   viewport, so scrollbars track the tile size. */
					max-height: 640px;
					}
					.cs-specific-widget table {
					border-collapse: separate; border-spacing: 0;
					width: 100%; font-size: 12px; color: var(--gray600);
					}
					/* Sticky column headers pin on the CELLS, not on thead:
					   several engines ignore position:sticky on table row
					   groups. They sit just below the toolbar band; the band's
					   real height is measured at runtime into the custom
					   property (38px fallback). */
					.cs-specific-widget thead th {
					position: -webkit-sticky; position: sticky;
					top: var(--cs-toolbar-h, 38px); z-index: 30;
					}
					/* Airy modern rows: generous padding, hairline separators
					   only - no zebra striping, the white breathes on its own. */
					.cs-specific-widget th, .cs-specific-widget td {
					text-align: left; vertical-align: middle;
					padding: 12px 14px; white-space: nowrap;
					border-bottom: 1px solid var(--gray200);
					}
					.cs-specific-widget tbody tr:hover { background: var(--primary-ghost); }
					/* ---------- Column filter row ----------
					   Revealed by the Filter button; pins just below the
					   column headers while rows scroll. */
					.cs-specific-widget tr.cs-filterrow td {
					position: -webkit-sticky; position: sticky;
					top: var(--cs-filter-top, 88px); z-index: 25;
					background: var(--white);
					padding: 6px 8px;
					border-bottom: 1px solid var(--gray300);
					}
					.cs-specific-widget .cs-colfilter-wrap { position: relative; }
					.cs-specific-widget .cs-colfilter {
					width: 100%; min-width: 60px; box-sizing: border-box;
					font-family: var(--font-family); font-size: 11px;
					color: var(--gray600); background: var(--gray100);
					border: 1px solid var(--gray300); border-radius: 6px;
					padding: 4px 20px 4px 8px; outline: none;
					}
					/* Little x inside a filled filter box: clears just that
					   column. Hidden while the box is empty. */
					.cs-specific-widget .cs-colfilter-x {
					position: absolute; right: 5px; top: 50%;
					transform: translateY(-50%);
					cursor: pointer; color: var(--gray450);
					font-size: 10px; font-weight: 700; line-height: 1;
					padding: 2px; display: none; user-select: none;
					}
					.cs-specific-widget .cs-colfilter-x:hover { color: var(--red600); }
					.cs-specific-widget .cs-colfilter:focus {
					border-color: var(--blue500); background: var(--white);
					}
					.cs-specific-widget tbody tr.cs-colfilter-hidden { display: none !important; }
					/* Funnel icon in each column header: click to filter that
					   column directly. Lights blue while its filter is active. */
					.cs-specific-widget .cs-fico {
					display: inline-block; margin-left: 6px; cursor: pointer;
					color: var(--gray450); line-height: 0; vertical-align: -1px;
					}
					.cs-specific-widget .cs-fico:hover { color: var(--blue600); }
					.cs-specific-widget .cs-fico.cs-fico-on { color: var(--blue500); }
					.cs-specific-widget .cs-fico svg { display: inline-block; vertical-align: middle; }
					/* Quick-filter funnel inside data cells: appears when you
					   hover the cell; clicking filters the grid to that value
					   (always faintly visible on touch screens, which have no
					   hover). */
					.cs-specific-widget td .cs-cellfico {
					display: inline-block; margin-left: 6px; cursor: pointer;
					color: var(--gray450); line-height: 0; vertical-align: -1px;
					opacity: 0; transition: opacity 0.12s ease, color 0.12s ease;
					}
					.cs-specific-widget td:hover .cs-cellfico { opacity: 1; }
					.cs-specific-widget td .cs-cellfico:hover { color: var(--blue600); }
					@media (hover: none) {
					.cs-specific-widget td .cs-cellfico { opacity: 0.55; }
					}
					/* ---------- Case-number hover preview ---------- */
					.cs-specific-widget .cs-tip {
					position: fixed; z-index: 90; display: none;
					max-width: 340px; padding: 12px 14px;
					background: var(--white); color: var(--gray600);
					border: 1px solid var(--card-border-color); border-radius: 10px;
					box-shadow: 0 8px 24px rgba(15, 92, 245, 0.16);
					font-size: 11.5px; line-height: 1.5; white-space: normal;
					pointer-events: none;
					}
					.cs-specific-widget .cs-tip-title { font-size: 12.5px; font-weight: 700; margin-bottom: 2px; }
					.cs-specific-widget .cs-tip-meta { color: var(--gray450); margin-bottom: 6px; }
					.cs-specific-widget .cs-tip-lbl {
					font-size: 10px; font-weight: 700; text-transform: uppercase;
					letter-spacing: 0.05em; color: var(--gray450); margin: 8px 0 1px 0;
					}
					.cs-specific-widget tbody tr:last-child td { border-bottom: none; }

					/* ---------- Toolbar ----------
					   A standalone band, direct child of the scroll wrap, so
					   it can pin in BOTH views. */
					.cs-specific-widget .cs-toolbar-band {
					position: -webkit-sticky; position: sticky; top: 0;
					/* left: 0 pins the band horizontally too - the table can
					   scroll sideways underneath, but the band (sized to the
					   visible area) stays put instead of being dragged off
					   the left edge and looking cut off. */
					left: 0; z-index: 40;
					background: var(--blue500); padding: 7px 10px;
					box-sizing: border-box;
					}
					/* Full widget width: the bar spans the whole table, not just
					   the width of its own controls. min-width keeps it filling
					   the visible area even when the table is narrower, and the
					   controls stay grouped at the left. */
					.cs-specific-widget .cs-toolbar {
					display: flex; align-items: center; gap: 8px;
					width: 100%; min-width: 100%;
					}
					.cs-specific-widget .cs-toolbar-title {
					font-size: 13px; font-weight: 600; color: var(--white);
					letter-spacing: -0.01em; padding-right: 6px;
					}
					.cs-specific-widget .cs-toolbar-right { margin-left: auto; }
					/* Shortcut letters: keep the underline close and tidy. */
					.cs-specific-widget .bt u {
					text-decoration: underline;
					text-underline-offset: 2px;
					}
					/* Narrow screens: the toolbar wraps into tidy rows instead
					   of pushing search/export/theme off the right edge, and
					   buttons never break across two lines. The sticky offsets
					   keep up because the band's height is measured live. */
					@media (max-width: 640px) {
					.cs-specific-widget .cs-toolbar { flex-wrap: wrap; row-gap: 6px; }
					}
					.cs-specific-widget .bt {
					font-family: var(--font-family); font-size: 11px; font-weight: 600;
					height: 24px; padding: 0 10px; margin-right: 4px;
					white-space: nowrap;
					color: var(--white); background: rgba(255,255,255,0.14);
					border: 1px solid rgba(255,255,255,0.55);
					border-radius: 4px; cursor: pointer;
					}
					.cs-specific-widget .bt:hover { background: rgba(255,255,255,0.28); }
					/* Pressed state for the My Cases / All Statuses view
					   buttons: solid white on the blue toolbar. */
					.cs-specific-widget .bt-active,
					.cs-specific-widget .bt-active:hover {
					background: var(--white); color: var(--blue600);
					border-color: var(--white);
					}
					/* Rows hidden by the view buttons. !important so the shared
					   search script re-showing a row inline can't resurface a
					   status the current view excludes. */
					.cs-specific-widget tbody tr.cs-mode-hidden { display: none !important; }
					/* Rows hidden by the search box. A separate class from the
					   view filter so search and My Cases compose instead of
					   clobbering each other. */
					.cs-specific-widget tbody tr.cs-search-hidden { display: none !important; }
					.cs-specific-widget div input.searchBox {
					font-family: var(--font-family); font-size: 11px;
					height: 24px; width: 170px; padding: 0 8px;
					color: var(--gray600); background: var(--white);
					border: 1px solid rgba(255,255,255,0.55); border-radius: 4px;
					box-sizing: border-box; line-height: normal;
					}
					.cs-specific-widget div input.searchBox:focus {
					outline: 2px solid var(--blue200); outline-offset: -1px;
					}
					/* The grouping dropdown works in both views: it groups the
					   TABLE's rows under subheadings, and regroups the BOARD's
					   columns by the chosen field ("No grouping" = the default
					   status columns). Styled with the same Prismatic tokens as
					   the other toolbar controls. */
					.cs-specific-widget select.cs-groupsel {
					/* appearance:none strips the OS chrome so the control can
					   carry the Prismatic look; the chevron is drawn back in as
					   an inline SVG (blue600, matching the link color - a data
					   URI cannot reference a CSS var, so this is the one place
					   the hex is written out). */
					-webkit-appearance: none; -moz-appearance: none; appearance: none;
					font-family: var(--font-family); font-size: 11px; font-weight: 600;
					height: 24px; padding: 0 24px 0 10px;
					color: var(--blue600); background-color: var(--white);
					background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%230048b0' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
					background-repeat: no-repeat;
					background-position: right 8px center;
					border: 1px solid rgba(255,255,255,0.55); border-radius: 4px;
					cursor: pointer;
					}
					.cs-specific-widget select.cs-groupsel:hover {
					border-color: var(--white);
					background-color: var(--blue100);
					}
					.cs-specific-widget select.cs-groupsel:focus {
					outline: 2px solid var(--blue200); outline-offset: -1px;
					}

					/* ---------- Column headers ----------
					   Pale blue header bar with dark blue text, matching the
					   header treatment in ClientSpace's own screens. */
					/* Column headers in the reference's voice: sentence case,
					   dark slate, bold, on plain white with a single hairline
					   underneath - quieter than the old blue uppercase band. */
					.cs-specific-widget .sticky-header {
					background: var(--white); color: var(--gray600);
					font-size: 12px; font-weight: 700;
					padding: 12px 14px;
					white-space: nowrap; border-bottom: 1px solid var(--gray300);
					}
					.cs-specific-widget .cs-sortable th { cursor: pointer; user-select: none; }
					/* Drag handle on each column header. A sticky th is a
					   positioned box, so the absolutely-placed handle rides
					   its right edge. The wide hit area is invisible; a thin
					   line appears on hover so the affordance is discoverable
					   without cluttering the header. */
					.cs-specific-widget .cs-resizer {
					position: absolute; top: 0; right: 0;
					width: 7px; height: 100%;
					cursor: col-resize;
					}
					.cs-specific-widget .cs-resizer:hover,
					.cs-specific-widget .cs-resizer.cs-resizing {
					box-shadow: inset -2px 0 0 var(--blue500);
					}
					.cs-specific-widget .cs-sortable .th-sort-asc::after  { content: "\25b4"; }
					.cs-specific-widget .cs-sortable .th-sort-desc::after { content: "\25be"; }
					.cs-specific-widget .cs-sortable .th-sort-asc::after,
					.cs-specific-widget .cs-sortable .th-sort-desc::after { margin-left: 5px; }
					.cs-specific-widget .cs-sortable .th-sort-asc,
					.cs-specific-widget .cs-sortable .th-sort-desc { color: var(--blue600); }

					/* ---------- Cell treatments ----------
					   Every data cell is the same 12px Inter as the table body;
					   cs-mono keeps only tabular digits so date/ID columns align.
					   (The old 11.5px override read as a second typeface.) */
					.cs-specific-widget .cs-mono {
					font-family: var(--mono);
					font-variant-numeric: tabular-nums;
					}
					.cs-specific-widget .cs-time { color: var(--gray500); }
					.cs-specific-widget .cs-muted { color: var(--gray450); }
					.cs-specific-widget .cs-casenum a {
					font-family: var(--mono); font-weight: 600;
					color: var(--link-color); text-decoration: none;
					}
					.cs-specific-widget .cs-casenum a:hover { text-decoration: underline; }
					.cs-specific-widget tbody a { color: var(--link-color); text-decoration: none; }
					.cs-specific-widget tbody a:hover { text-decoration: underline; }
					.cs-specific-widget .cs-subject { white-space: normal; min-width: 200px; }
					.cs-specific-widget .cs-longtext { white-space: normal; }
					.cs-specific-widget .cs-clamp {
					display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
					overflow: hidden; max-width: 260px; color: var(--gray500);
					}
					.cs-specific-widget td.cs-overdue {
					color: var(--red700); font-weight: 600;
					background: var(--red100); box-shadow: inset 2px 0 0 var(--red600);
					}
					/* Due within the next 7 days (and not closed): amber, one
					   step down from the overdue red. */
					.cs-specific-widget td.cs-duesoon {
					color: var(--orange700); font-weight: 600;
					background: var(--yellow100); box-shadow: inset 2px 0 0 var(--yellow600);
					}
					.cs-specific-widget .cs-empty {
					padding: 32px 12px; text-align: center; color: var(--gray450);
					white-space: normal;
					}
					/* Illustrated empty state: soft green disc with a check. */
					.cs-specific-widget .cs-empty-ico {
					width: 56px; height: 56px; border-radius: 50%;
					background: var(--green100);
					display: inline-flex; align-items: center; justify-content: center;
					}
					.cs-specific-widget .cs-empty-title {
					font-size: 13px; font-weight: 600; color: var(--gray600);
					margin-top: 10px;
					}
					.cs-specific-widget .cs-board-empty {
					flex: 1 1 100%; text-align: center; padding: 32px 12px;
					}

					/* ---------- Chips ---------- */
					.cs-specific-widget .cs-chip {
					display: inline-block; padding: 1px 8px; border-radius: 10px;
					font-size: 10.5px; font-weight: 600; line-height: 16px;
					white-space: nowrap; border: 1px solid transparent;
					}
					.cs-specific-widget .cs-chip-green  { background: var(--green100);  color: var(--green800);  border-color: var(--green200); }
					.cs-specific-widget .cs-chip-amber  { background: var(--yellow100); color: var(--orange700); border-color: var(--yellow300); }
					.cs-specific-widget .cs-chip-blue   { background: var(--blue100);   color: var(--blue600);   border-color: var(--blue200); }
					.cs-specific-widget .cs-chip-red    { background: var(--red100);    color: var(--red700);    border-color: var(--red200); }
					.cs-specific-widget .cs-chip-purple { background: var(--purple100); color: var(--purple700); border-color: var(--purple200); }
					.cs-specific-widget .cs-chip-gray   { background: var(--gray200);   color: var(--gray500);   border-color: var(--gray300); }

					/* ---------- Group-by rows (table view) ----------
					   Prominent section bands: pale blue with a solid left
					   accent and the count in a pill, echoing the column
					   header treatment so groups read as structure, not data. */
					.cs-specific-widget td.cs-grouphead {
					background: var(--gray150); color: var(--gray600);
					font-weight: 700; font-size: 12px;
					text-transform: uppercase; letter-spacing: 0.04em;
					border-bottom: 1px solid var(--gray300);
					box-shadow: inset 3px 0 0 var(--gray450);
					padding: 8px 12px;
					}
					.cs-specific-widget .cs-grouphead-count {
					display: inline-block; margin-left: 8px;
					background: var(--white); color: var(--gray600);
					border: 1px solid var(--gray300); border-radius: 10px;
					padding: 0 8px; font-size: 10px; font-weight: 600;
					line-height: 16px; letter-spacing: 0;
					}
					/* Click a group header to fold/unfold its rows; the
					   chevron shows which state it is in. */
					.cs-specific-widget tr.cs-grouprow { cursor: pointer; user-select: none; }
					.cs-specific-widget td.cs-grouphead::before {
					content: "\25be"; margin-right: 8px; font-size: 10px;
					}
					.cs-specific-widget tr.cs-group-collapsed td.cs-grouphead::before { content: "\25b8"; }
					.cs-specific-widget tbody tr.cs-group-hidden { display: none !important; }
					.cs-specific-widget tr.cs-group-empty { display: none; }

					/* ---------- Board (kanban) view ----------
					   Toggled by the Board button: cs-boardmode on the widget
					   root hides the table body and column headers (the sticky
					   toolbar stays) and shows one column per status bucket. */
					.cs-specific-widget .cs-board { display: none; padding: 10px; gap: 10px; align-items: flex-start; flex-wrap: wrap; }
					.cs-specific-widget.cs-boardmode .cs-board { display: flex; }
					/* Full-width strip above the columns telling the user the
					   board is read-only (no drag and drop). */
					.cs-specific-widget .cs-board-note {
					flex: 1 1 100%;
					background: var(--blue100); color: var(--blue700);
					border: 1px solid var(--blue200); border-radius: 6px;
					font-size: 11px; padding: 6px 10px;
					}
					.cs-specific-widget.cs-boardmode .cs-sortable { display: none; }
					.cs-specific-widget.cs-cardsmode .cs-sortable,
					.cs-specific-widget.cs-cardsmode .cs-board { display: none; }
					/* With the table hidden nothing stretches the widget, so
					   it shrink-wraps to zero. Pin the widths in cards mode. */
					.cs-specific-widget.cs-cardsmode { width: 100%; box-sizing: border-box; }
					.cs-specific-widget.cs-cardsmode .cs-tablewrap { width: 100%; box-sizing: border-box; }
					.cs-specific-widget .cs-cardswrap { min-width: 320px; }
					/* Cards carousel: one large card at a time. Cards are
					   show/hide (not absolutely stacked) so the wrap takes the
					   active card's natural height, and the prev/next controls
					   sit in a footer row instead of hanging off the edges
					   where the scroll wrap would clip them. */
					.cs-specific-widget .cs-cardswrap {
					display: none; position: relative; width: 100%;
					max-width: 900px; margin: 16px auto; padding: 0 10px 16px 10px;
					box-sizing: border-box;
					}
					/* Two panes: the case list on the left, the big card on the
					   right. flex-wrap lets the list stack on top on phones. */
					.cs-cardswrap .cs-cardsbody {
					display: flex; gap: 14px; align-items: flex-start; flex-wrap: wrap;
					}
					.cs-cardswrap .cs-cardlist {
					flex: 0 1 250px; min-width: 210px; max-height: 430px;
					overflow-y: auto; padding: 6px;
					background: var(--gray100);
					border: 1px solid var(--card-border-color);
					border-radius: var(--card-border-radius);
					box-sizing: border-box;
					}
					.cs-cardswrap .cs-cardmain { flex: 1 1 340px; min-width: 300px; }
					.cs-cardlist .cs-cardlist-item {
					display: flex; align-items: center; gap: 8px;
					padding: 7px 9px; border-radius: 6px; cursor: pointer;
					user-select: none;
					transition: background 0.15s ease, transform 0.15s ease;
					}
					.cs-cardlist .cs-cardlist-item:hover {
					background: var(--blue100);
					transform: translateX(3px);
					}
					.cs-cardlist .cs-cardlist-dot {
					width: 8px; height: 8px; border-radius: 50%; flex: none;
					background: var(--gray450);
					}
					.cs-cardlist .cs-cardlist-dot-new  { background: var(--blue500); }
					.cs-cardlist .cs-cardlist-dot-prog { background: var(--yellow600); }
					.cs-cardlist .cs-cardlist-dot-hold { background: var(--purple500); }
					.cs-cardlist .cs-cardlist-dot-late { background: var(--orange600); }
					.cs-cardlist .cs-cardlist-dot-done { background: var(--green600); }
					.cs-cardlist .cs-cardlist-num {
					font-weight: 700; color: var(--blue600); flex: none; font-size: 12px;
					}
					.cs-cardlist .cs-cardlist-subj {
					font-size: 11px; color: var(--gray600); min-width: 0;
					white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
					}
					.cs-cardlist .cs-cardlist-group {
					padding: 10px 9px 4px 9px;
					font-size: 9.5px; font-weight: 700; text-transform: uppercase;
					letter-spacing: 0.06em; color: var(--blue700);
					}
					.cs-cardlist .cs-cardlist-group:first-child { padding-top: 4px; }
					/* Mobile-only toggle bar above the case list. Hidden on
					   desktop, where the two-pane layout has room for both. */
					.cs-cardswrap .cs-cardlist-toggle { display: none; }
					.cs-cardswrap .cs-cardlist-toggle-count {
					display: inline-block; background: var(--blue100); color: var(--blue600);
					border-radius: 10px; padding: 0 8px;
					font-size: 10px; line-height: 16px; font-weight: 700;
					}
					@media (max-width: 640px) {
					.cs-cardswrap .cs-cardlist-toggle {
					display: flex; align-items: center; gap: 8px;
					flex: 1 1 100%;
					background: var(--white); border: 1px solid var(--card-border-color);
					border-radius: 10px; padding: 10px 12px;
					font-size: 12px; font-weight: 700; color: var(--gray600);
					cursor: pointer; user-select: none;
					}
					.cs-cardswrap .cs-cardlist-toggle .cs-chevron {
					margin-left: auto; font-size: 11px; color: var(--gray450);
					}
					.cs-cardswrap.cs-list-collapsed .cs-cardlist-toggle .cs-chevron {
					transform: rotate(-90deg);
					}
					.cs-cardswrap .cs-cardlist { flex: 1 1 100%; max-height: 300px; }
					.cs-cardswrap.cs-list-collapsed .cs-cardlist { display: none; }
					}
					/* Polished scrollbar for the list and detail panels: a
					   soft rounded channel with a pill thumb riding inside
					   it. The thumb's border matches the channel so it looks
					   inset; it deepens on hover and warms to blue while
					   dragging. Theme tokens keep it right in dark mode. */
					.cs-cardswrap .cs-cardlist,
					.cs-cardswrap .cs-tcard-sectxt {
					scrollbar-width: thin;
					scrollbar-color: var(--gray300) var(--gray150);
					}
					.cs-cardswrap .cs-cardlist::-webkit-scrollbar,
					.cs-cardswrap .cs-tcard-sectxt::-webkit-scrollbar { width: 10px; }
					.cs-cardswrap .cs-cardlist::-webkit-scrollbar-track,
					.cs-cardswrap .cs-tcard-sectxt::-webkit-scrollbar-track {
					background: var(--gray150);
					border-radius: 10px;
					margin: 6px 0;
					}
					.cs-cardswrap .cs-cardlist::-webkit-scrollbar-thumb,
					.cs-cardswrap .cs-tcard-sectxt::-webkit-scrollbar-thumb {
					background: var(--gray300); border-radius: 10px;
					border: 2px solid var(--gray150);
					min-height: 32px;
					}
					.cs-cardswrap .cs-cardlist::-webkit-scrollbar-thumb:hover,
					.cs-cardswrap .cs-tcard-sectxt::-webkit-scrollbar-thumb:hover {
					background: var(--gray450);
					}
					.cs-cardswrap .cs-cardlist::-webkit-scrollbar-thumb:active,
					.cs-cardswrap .cs-tcard-sectxt::-webkit-scrollbar-thumb:active {
					background: var(--blue500);
					}
					.cs-cardswrap .cs-cardlist::-webkit-scrollbar-corner,
					.cs-cardswrap .cs-tcard-sectxt::-webkit-scrollbar-corner {
					background: transparent;
					}
					.cs-cardlist .cs-cardlist-item.cs-cardlist-active { background: var(--blue500); }
					.cs-cardlist .cs-cardlist-item.cs-cardlist-active .cs-cardlist-num,
					.cs-cardlist .cs-cardlist-item.cs-cardlist-active .cs-cardlist-subj {
					color: var(--white);
					}
					.cs-cardlist .cs-cardlist-item.cs-cardlist-active .cs-cardlist-dot {
					background: var(--white);
					animation: csDotPulse 1.8s ease-out infinite;
					}
					@keyframes csDotPulse {
					0% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.55); }
					70% { box-shadow: 0 0 0 7px rgba(255, 255, 255, 0); }
					100% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); }
					}
					.cs-specific-widget.cs-cardsmode .cs-cardswrap { display: block; }
					.cs-cardswrap .cs-card-container { position: relative; width: 100%; }
					.cs-cardswrap .cs-card-single { display: none; }
					.cs-cardswrap .cs-card-single.cs-card-active { display: block; }
					/* ---------- Motion ----------
					   Cards slide in from the side you're flipping toward,
					   with a whisper of tilt; buttons and list rows nudge on
					   hover; the counter pops; the active dot pulses. All
					   transform/opacity only (GPU-cheap), and everything is
					   switched off for reduced-motion users. */
					.cs-cardswrap .cs-card-single.cs-card-in-next { animation: csCardInNext 0.32s cubic-bezier(0.22, 1, 0.36, 1); }
					.cs-cardswrap .cs-card-single.cs-card-in-prev { animation: csCardInPrev 0.32s cubic-bezier(0.22, 1, 0.36, 1); }
					@keyframes csCardInNext {
					from { opacity: 0; transform: translateX(46px) rotate(0.7deg) scale(0.97); }
					to { opacity: 1; transform: none; }
					}
					@keyframes csCardInPrev {
					from { opacity: 0; transform: translateX(-46px) rotate(-0.7deg) scale(0.97); }
					to { opacity: 1; transform: none; }
					}
					.cs-cardswrap .cs-bigcard { transition: transform 0.2s ease, box-shadow 0.2s ease; }
					.cs-cardswrap .cs-bigcard:hover {
					transform: translateY(-3px);
					box-shadow: 0 10px 22px rgba(15, 92, 245, 0.15);
					}
					.cs-cardswrap .cs-card-nav { transition: transform 0.15s ease, background 0.15s ease; }
					.cs-cardswrap .cs-card-nav:hover { transform: translateY(-1px) scale(1.04); }
					.cs-cardswrap .cs-card-nav:active { transform: scale(0.93); }
					.cs-cardswrap .cs-card-counter.cs-pop { animation: csCounterPop 0.25s ease; }
					@keyframes csCounterPop {
					0% { transform: scale(1); }
					50% { transform: scale(1.18); }
					100% { transform: scale(1); }
					}
					.cs-cardswrap .cs-bigcard.cs-showmore .cs-bigcard-details { animation: csDetailsIn 0.25s ease; }
					@keyframes csDetailsIn {
					from { opacity: 0; transform: translateY(-6px); }
					to { opacity: 1; transform: none; }
					}
					@media (prefers-reduced-motion: reduce) {
					.cs-cardswrap * { animation: none !important; transition: none !important; }
					}
					/* Carousel cards use a helpdesk-ticket layout: initials
					   avatar + name header with a muted case number and the
					   priority pill on the right, a muted created line, a
					   "Sub :" subject, then a labeled Assignee / Status / Due
					   footer row - soft corners, quiet shadow. */
					.cs-cardswrap .cs-bigcard {
					background: var(--white);
					border: 1px solid var(--card-border-color);
					border-radius: 14px;
					box-shadow: 0 4px 14px rgba(15, 92, 245, 0.08);
					padding: 18px 20px 12px 20px;
					}
					.cs-cardswrap .cs-tcard-head { display: flex; gap: 12px; align-items: flex-start; }
					.cs-cardswrap .cs-tcard-avatar {
					width: 44px; height: 44px; border-radius: 50%; flex: none;
					display: flex; align-items: center; justify-content: center;
					font-size: 15px; font-weight: 700; letter-spacing: 0.02em;
					}
					.cs-cardswrap .cs-av-new  { background: var(--blue100); color: var(--blue600); }
					.cs-cardswrap .cs-av-prog { background: var(--yellow100); color: var(--orange700); }
					.cs-cardswrap .cs-av-hold { background: var(--purple100); color: var(--purple500); }
					.cs-cardswrap .cs-av-late { background: var(--red100); color: var(--red600); }
					.cs-cardswrap .cs-av-done { background: var(--green100); color: var(--green600); }
					.cs-cardswrap .cs-tcard-hmain { flex: 1 1 auto; min-width: 0; }
					.cs-cardswrap .cs-tcard-nameline {
					display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
					}
					.cs-cardswrap .cs-tcard-name { font-size: 15px; font-weight: 700; }
					.cs-cardswrap .cs-tcard-caseno a {
					color: var(--gray450); font-weight: 600; font-size: 13px;
					text-decoration: none;
					}
					.cs-cardswrap .cs-tcard-caseno a:hover {
					color: var(--link-color); text-decoration: underline;
					}
					.cs-cardswrap .cs-tcard-pill { margin-left: auto; }
					.cs-cardswrap .cs-tcard-time { font-size: 11px; color: var(--gray450); margin-top: 3px; }
					.cs-cardswrap .cs-tcard-sub {
					margin-top: 14px; font-size: 13px; font-weight: 600;
					line-height: 1.4; white-space: normal;
					/* Very long subjects clamp at three lines; hover shows
					   the full text via the title attribute. */
					display: -webkit-box; -webkit-line-clamp: 3;
					-webkit-box-orient: vertical; overflow: hidden;
					overflow-wrap: anywhere;
					}
					.cs-cardswrap .cs-tcard-sublbl { color: var(--gray450); }
					.cs-cardswrap .cs-bigcard .cs-bcard-tags { margin: 8px 0 0 0; }
					.cs-cardswrap .cs-tcard-foot {
					display: flex; justify-content: space-between; gap: 12px;
					flex-wrap: wrap; margin-top: 16px; padding-top: 14px;
					border-top: 1px solid var(--gray200);
					}
					.cs-cardswrap .cs-tcard-col { display: flex; flex-direction: column; gap: 7px; min-width: 0; }
					.cs-cardswrap .cs-tcard-lbl { font-size: 11.5px; color: var(--gray450); }
					.cs-cardswrap .cs-tcard-val {
					display: flex; align-items: center; gap: 7px;
					font-size: 12.5px; font-weight: 600;
					}
					.cs-cardswrap .cs-tcard-minav {
					width: 20px; height: 20px; border-radius: 50%; flex: none;
					display: flex; align-items: center; justify-content: center;
					font-size: 9px; font-weight: 700;
					background: var(--blue100); color: var(--blue600);
					}
					.cs-cardswrap .cs-tcard-dot {
					width: 9px; height: 9px; border-radius: 50%; flex: none;
					background: currentColor;
					}
					.cs-cardswrap .cs-tst-new  { color: var(--blue600); }
					.cs-cardswrap .cs-tst-prog { color: var(--yellow600); }
					.cs-cardswrap .cs-tst-hold { color: var(--purple500); }
					.cs-cardswrap .cs-tst-late { color: var(--red600); }
					.cs-cardswrap .cs-tst-done { color: var(--green600); }
					.cs-cardswrap .cs-tcard-cal svg { display: block; color: var(--gray450); }
					.cs-cardswrap .cs-bigcard-badge {
					display: inline-block; margin-left: 6px; padding: 0 6px;
					background: var(--red100); color: var(--red700);
					border-radius: 8px; font-size: 10px; font-weight: 700;
					line-height: 16px; vertical-align: 1px;
					}
					.cs-cardswrap .cs-bigcard-more {
					display: flex; align-items: center; gap: 6px;
					margin-top: 12px; padding: 8px 0 4px 0; width: 100%;
					background: none; border: none; border-top: 1px solid var(--gray200);
					color: var(--blue600); font-weight: 600; font-size: 12px;
					font-family: inherit; cursor: pointer; text-align: left;
					}
					.cs-cardswrap .cs-bigcard-more:hover { color: var(--blue700); }
					.cs-cardswrap .cs-chevron {
					display: inline-block; font-size: 10px;
					transition: transform 0.2s ease;
					}
					.cs-cardswrap .cs-bigcard.cs-showmore .cs-chevron { transform: rotate(180deg); }
					.cs-cardswrap .cs-bigcard-details {
					display: none; margin-top: 6px; padding-top: 4px;
					}
					.cs-cardswrap .cs-bigcard.cs-showmore .cs-bigcard-details { display: block; }
					/* Each detail is a quiet rounded panel in the card's own
					   voice: sentence-case label, readable body, long text
					   wraps and scrolls instead of stretching the card. */
					.cs-cardswrap .cs-tcard-sec {
					background: var(--gray100); border: 1px solid var(--gray200);
					border-radius: 10px; padding: 10px 12px; margin-bottom: 8px;
					}
					.cs-cardswrap .cs-tcard-sec:last-child { margin-bottom: 0; }
					.cs-cardswrap .cs-tcard-seclbl {
					font-size: 11.5px; color: var(--gray450); margin-bottom: 4px;
					}
					.cs-cardswrap .cs-tcard-sectxt {
					font-size: 12.5px; line-height: 1.55; color: var(--gray600);
					white-space: pre-line; overflow-wrap: anywhere;
					max-height: 180px; overflow-y: auto;
					}
					.cs-cardswrap .cs-card-controls {
					display: flex; align-items: center; justify-content: center;
					gap: 14px; margin-top: 12px;
					}
					.cs-cardswrap .cs-card-nav {
					cursor: pointer; background: var(--blue500); color: var(--white);
					border: none; padding: 6px 16px; border-radius: 6px;
					font-size: 16px; line-height: 1.2; user-select: none;
					}
					.cs-cardswrap .cs-card-nav:hover { background: var(--blue600); }
					.cs-cardswrap .cs-card-counter {
					font-size: 12px; color: var(--gray600); font-weight: 600;
					min-width: 90px; text-align: center;
					}
					/* Lanes and cards follow the Prismatic card language: token
					   borders and radii, sunk-gray lane over white cards, color
					   confined to the lane's top accent and the chips. */
					.cs-specific-widget .cs-bcol {
					flex: 1 1 200px; min-width: 200px;
					background: var(--gray150);
					border: 1px solid var(--card-border-color);
					border-radius: var(--card-border-radius);
					border-top: 3px solid var(--gray300);
					padding: 8px;
					}
					.cs-specific-widget .cs-bcol-new  { border-top-color: var(--blue500); }
					.cs-specific-widget .cs-bcol-prog { border-top-color: var(--yellow600); }
					.cs-specific-widget .cs-bcol-hold { border-top-color: var(--purple500); }
					.cs-specific-widget .cs-bcol-late { border-top-color: var(--orange600); }
					/* Lane headers scroll naturally with their lane. They were
					   briefly sticky, but when lanes wrap into a vertical
					   stack (narrow tiles, phones) each header pinned
					   mid-screen and floated over cards. The toolbar band
					   stays as the one pinned chrome in board view. */
					.cs-specific-widget .cs-bcol-head {
					display: flex; justify-content: space-between; align-items: center; gap: 8px;
					font-size: 10px; font-weight: 600; text-transform: uppercase;
					letter-spacing: 0.05em; color: var(--gray600);
					margin-bottom: 8px;
					}
					.cs-specific-widget .cs-bcol-count {
					background: var(--white); color: var(--gray500);
					border: 1px solid var(--card-border-color);
					border-radius: 10px; padding: 0 7px;
					font-size: 10px; line-height: 16px; letter-spacing: 0;
					}
					.cs-specific-widget .cs-bcard {
					background: var(--white);
					border: 1px solid var(--card-border-color);
					border-radius: var(--card-border-radius);
					padding: 8px 10px; margin-bottom: 8px;
					transition: border-color 0.12s ease, box-shadow 0.12s ease;
					}
					/* Hover is a shadow only - the blue border is reserved for
					   the keyboard selection, so the two can't be confused. */
					.cs-specific-widget .cs-bcard:hover {
					box-shadow: 0 2px 6px var(--gray300);
					}
					.cs-specific-widget .cs-bcard:last-child { margin-bottom: 0; }
					.cs-specific-widget .cs-bcard a { color: var(--link-color); font-weight: 600; text-decoration: none; }
					.cs-specific-widget .cs-bcard a:hover { text-decoration: underline; }
					.cs-specific-widget .cs-bcard-subj { margin: 4px 0; white-space: normal; }
					.cs-specific-widget .cs-bcard-tags {
					display: flex; flex-wrap: wrap; gap: 4px;
					margin: 4px 0 6px 0;
					}
					.cs-specific-widget .cs-bcard-meta {
					display: flex; flex-wrap: wrap; gap: 2px 10px;
					font-size: 11px; color: var(--gray500); white-space: normal;
					}
					.cs-specific-widget .cs-bcard-due-late { color: var(--red700); font-weight: 600; }
					/* Collapsible board columns: click a column header to fold
					   it to a slim vertical bar and give the rest more room. */
					.cs-specific-widget .cs-bcol-head { cursor: pointer; user-select: none; }
					.cs-specific-widget .cs-bcol-collapsed { flex: 0 0 36px; min-width: 36px; }
					.cs-specific-widget .cs-bcol-collapsed .cs-bcard { display: none; }
					.cs-specific-widget .cs-bcol-collapsed .cs-bcol-head {
					writing-mode: vertical-rl;
					justify-content: flex-start; gap: 6px; margin-bottom: 0;
					}
					/* Inside a collapsed bar the count sheds its pill styling
					   and stays upright, so it can't run off the slim column. */
					.cs-specific-widget .cs-bcol-collapsed .cs-bcol-count {
					writing-mode: horizontal-tb;
					background: none; border: none; padding: 0;
					color: var(--gray600);
					}

					/* ---------- Relative-time badges on due dates ---------- */
					.cs-specific-widget .cs-duebadge {
					display: block; font-size: 10px; font-weight: 600;
					margin-top: 1px; opacity: 0.85;
					}

					/* ---------- Row expansion (click a row for full text) ---------- */
					.cs-specific-widget .cs-sortable tbody tr { cursor: pointer; }
					.cs-specific-widget td.cs-expandcell {
					background: var(--gray100); white-space: normal;
					padding: 10px 14px; cursor: default;
					box-shadow: inset 3px 0 0 var(--blue200);
					}
					.cs-specific-widget .cs-expandsec { margin-bottom: 8px; }
					.cs-specific-widget .cs-expandsec:last-child { margin-bottom: 0; }
					.cs-specific-widget .cs-expandlbl {
					font-size: 10px; font-weight: 600; text-transform: uppercase;
					letter-spacing: 0.06em; color: var(--gray450); margin-bottom: 2px;
					}
					.cs-specific-widget .cs-expandtxt {
					font-size: 12px; color: var(--gray600);
					white-space: pre-wrap; max-width: 720px;
					}


					/* ---------- Keyboard navigation highlight ----------
					   Deliberately distinct from the group-header bands (pale
					   blue, flat): the selected row is a deeper blue with a
					   solid ring along its top, bottom, and left edges, so
					   "where I am" never blends into "section label". */
					.cs-specific-widget tbody tr.cs-kbd td {
					background: var(--blue200);
					box-shadow: inset 0 2px 0 var(--blue500), inset 0 -2px 0 var(--blue500);
					}
					.cs-specific-widget tbody tr.cs-kbd td:first-of-type {
					box-shadow: inset 2px 0 0 var(--blue500), inset 0 2px 0 var(--blue500), inset 0 -2px 0 var(--blue500);
					}
					/* Board-view counterpart: the keyboard-selected card is
					   unmistakable - solid blue border, pale blue fill. */
					.cs-specific-widget .cs-bcard.cs-kbd-card {
					border-color: var(--blue500);
					background: var(--blue100);
					box-shadow: 0 0 0 2px var(--blue200);
					}

				</style>
			</head>
			<body>
				<div class="cs-specific-widget">
					<xsl:choose>
						<!-- The widget engine only emits a table node for result sets that
						     actually returned rows, so a proc that matched nothing hands this
						     stylesheet an empty xml root with no table child at all. Both
						     branches below render the same KPI strip and toolbar; only the data
						     branch has rows to summarize and a client to describe. -->
						<xsl:when test="xml/table">
							<xsl:for-each select="xml/table">

								<xsl:variable name="r1" select="./row[1]"/>
								<xsl:variable name="total" select="count(./row)"/>

								<!-- The client columns never appear in the table - they are
								     always collapsed into the context card. $singleClient
								     only decides WHAT the card can say: with @CurrentWorkspace
								     supplied every row is the same client, so the card names
								     it. If the widget is ever run without project context the
								     rows span clients and no single client can be named, so
								     the card degrades to a count instead of showing row 1's
								     client as though it applied to all of them. -->
								<xsl:variable name="clientKey" select="normalize-space(concat($r1/ClientNumber, '~', $r1/LegalName, $r1/Name))"/>
								<xsl:variable name="hasClient" select="count($r1/ClientNumber) + count($r1/LegalName) + count($r1/Name) &gt; 0"/>
								<xsl:variable name="singleClient" select="$hasClient and count(./row[normalize-space(concat(ClientNumber, '~', LegalName, Name)) != $clientKey]) = 0"/>

								<!-- KPI buckets. New and On Hold are explicit status lists;
								     In Progress is everything active that is neither - so an
								     unrecognized status counts as In Progress rather than
								     vanishing. Past Due is date-based and filled in by
								     markOverdue() at render time. -->
								<xsl:variable name="closed" select="count(./row[contains($notOpenStatuses, concat('|', translate(normalize-space(CaseStatus | Status), $uc, $lc), '|'))])"/>
								<xsl:variable name="newCount" select="count(./row[contains($blueStatuses, concat('|', translate(normalize-space(CaseStatus | Status), $uc, $lc), '|'))])"/>
								<xsl:variable name="holdCount" select="count(./row[contains($holdStatuses, concat('|', translate(normalize-space(CaseStatus | Status), $uc, $lc), '|'))])"/>
								<xsl:variable name="progCount" select="$total - $closed - $newCount - $holdCount"/>

								<!-- ============ Client context card ============
								     Holds LegalName, ClientNumber, ClientStatus, ImportID,
								     ContractType, ContractEffectiveDate and DateOf1stPayroll.
								     These are deliberately NOT table columns. -->
								<xsl:if test="$hasClient and $total &gt; 0">
									<div class="cs-clientcard">
										<div class="cs-cc-head">
											<div class="cs-cc-name">
												<xsl:choose>
													<xsl:when test="not($singleClient)">
														<xsl:text>Multiple Clients</xsl:text>
													</xsl:when>
													<xsl:when test="number($r1/pkClientMasterID) &gt; 0">
														<xsl:element name="a">
															<xsl:attribute name="href">#</xsl:attribute>
															<xsl:attribute name="data-url">
																<xsl:value-of select="$clientRoute"/>
																<xsl:text>/</xsl:text>
																<xsl:value-of select="$clientProjType"/>
																<xsl:text>/</xsl:text>
																<xsl:value-of select="normalize-space($r1/pkClientMasterID)"/>
															</xsl:attribute>
															<xsl:call-template name="value-or-dash">
																<xsl:with-param name="value" select="$r1/LegalName | $r1/Name"/>
															</xsl:call-template>
														</xsl:element>
													</xsl:when>
													<xsl:otherwise>
														<xsl:call-template name="value-or-dash">
															<xsl:with-param name="value" select="$r1/LegalName | $r1/Name"/>
														</xsl:call-template>
													</xsl:otherwise>
												</xsl:choose>
											</div>
											<xsl:if test="$singleClient and normalize-space($r1/ClientNumber) != ''">
												<span class="cs-chip cs-chip-blue">
													<xsl:value-of select="normalize-space($r1/ClientNumber)"/>
												</span>
											</xsl:if>
											<xsl:if test="$singleClient and normalize-space($r1/ClientStatus) != ''">
												<xsl:call-template name="chip">
													<xsl:with-param name="value" select="$r1/ClientStatus"/>
													<xsl:with-param name="scale" select="'clientstatus'"/>
												</xsl:call-template>
											</xsl:if>
										</div>
										<!-- Field grid is only meaningful for one client; with
										     rows spanning clients it would attribute row 1's
										     contract details to everybody. -->
										<xsl:if test="$singleClient">
											<div class="cs-cc-grid">
												<xsl:call-template name="cc-field">
													<xsl:with-param name="label" select="'Import ID'"/>
													<xsl:with-param name="value" select="$r1/ImportID"/>
													<xsl:with-param name="mono" select="1"/>
												</xsl:call-template>
												<xsl:call-template name="cc-field">
													<xsl:with-param name="label" select="'Contract Type'"/>
													<xsl:with-param name="value" select="$r1/ContractType"/>
												</xsl:call-template>
												<xsl:call-template name="cc-field">
													<xsl:with-param name="label" select="'Contract Effective'"/>
													<xsl:with-param name="value" select="$r1/ContractEffectiveDate"/>
													<xsl:with-param name="mono" select="1"/>
													<xsl:with-param name="date" select="1"/>
												</xsl:call-template>
												<xsl:call-template name="cc-field">
													<xsl:with-param name="label" select="'Date of 1st Payroll'"/>
													<xsl:with-param name="value" select="$r1/DateOf1stPayroll | $r1/FirstPayrollDate"/>
													<xsl:with-param name="mono" select="1"/>
													<xsl:with-param name="date" select="1"/>
												</xsl:call-template>
											</div>
										</xsl:if>
									</div>
								</xsl:if>

								<xsl:call-template name="kpi-strip">
									<xsl:with-param name="total" select="$total"/>
									<xsl:with-param name="new"   select="$newCount"/>
									<xsl:with-param name="prog"  select="$progCount"/>
									<xsl:with-param name="hold"  select="$holdCount"/>
								</xsl:call-template>

								<!-- ============ Case table ============ -->
								<div class="cs-tablewrap">
									<xsl:call-template name="case-toolbar"/>
									<!-- cs-sortable: sorting is self-owned (csSortByHeader in the
									     script below), like search and export. Renamed away from
									     "table-sortable" so the shared helper script cannot bind
									     a second competing sort to the same header clicks. -->
									<table class="cs-sortable">
										<thead>
											<tr>
												<xsl:for-each select="./row[1]/*">
													<xsl:variable name="columnName" select="name()"/>
													<xsl:choose>
														<!-- hidden keys, plus every client-level column: those
														     live in the context card, never in the table -->
														<xsl:when test="contains($hiddenCols, concat('|', name(), '|'))
																		 or contains($clientCols, concat('|', name(), '|'))">
															<th style="display: none;"/>
														</xsl:when>

														<xsl:when test="name()='CaseNumber'">
															<th class="sticky-header">
																<xsl:text>Case #</xsl:text>
															</th>
														</xsl:when>
														<xsl:when test="name()='CaseStatus' or name()='Status'">
															<th class="sticky-header">
																<xsl:text>Status</xsl:text>
															</th>
														</xsl:when>
														<xsl:when test="name()='Priority'">
															<th class="sticky-header">
																<xsl:text>Priority</xsl:text>
															</th>
														</xsl:when>
														<xsl:when test="name()='Category'">
															<th class="sticky-header">
																<xsl:text>Category</xsl:text>
															</th>
														</xsl:when>
														<xsl:when test="name()='CaseType'">
															<th class="sticky-header">
																<xsl:text>Case Type</xsl:text>
															</th>
														</xsl:when>
														<xsl:when test="name()='Subject'">
															<th class="sticky-header">
																<xsl:text>Subject</xsl:text>
															</th>
														</xsl:when>
														<xsl:when test="name()='AssignedTo'">
															<th class="sticky-header">
																<xsl:text>Assigned To</xsl:text>
															</th>
														</xsl:when>
														<xsl:when test="name()='Employee'">
															<th class="sticky-header">
																<xsl:text>Employee</xsl:text>
															</th>
														</xsl:when>
														<xsl:when test="name()='DueDate'">
															<th class="sticky-header">
																<xsl:text>Due</xsl:text>
															</th>
														</xsl:when>
														<xsl:when test="name()='CreateDate'">
															<th class="sticky-header">
																<xsl:text>Created</xsl:text>
															</th>
														</xsl:when>
														<xsl:when test="name()='CreateTime'">
															<th class="sticky-header">
																<xsl:text>Created Time</xsl:text>
															</th>
														</xsl:when>
														<xsl:when test="name()='Issue'">
															<th class="sticky-header">
																<xsl:text>Issue</xsl:text>
															</th>
														</xsl:when>
														<xsl:when test="name()='Description'">
															<th class="sticky-header">
																<xsl:text>Description</xsl:text>
															</th>
														</xsl:when>
														<xsl:when test="name()='Resolution'">
															<th class="sticky-header">
																<xsl:text>Resolution</xsl:text>
															</th>
														</xsl:when>

														<xsl:otherwise>
															<th class="sticky-header">
																<xsl:value-of select="$columnName"/>
															</th>
														</xsl:otherwise>
													</xsl:choose>
												</xsl:for-each>
											</tr>
										</thead>

										<tbody>
											<xsl:if test="$total = 0">
												<tr>
													<td class="cs-empty" colspan="100%">
											<span class="cs-empty-ico">
												<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="stroke: var(--green600);">
													<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
													<polyline points="22 4 12 14.01 9 11.27"/>
												</svg>
											</span>
											<div class="cs-empty-title">No open cases</div>
										</td>
												</tr>
											</xsl:if>

											<xsl:for-each select="./row">
												<tr>
													<!-- The assigned-to id rides on the row rather than in the
													     hidden cell: hidden cells render empty (see the
													     $hiddenCols branch below), and an attribute is also
													     invisible to the shared search box and to the CSV
													     export, so filtering by user cannot leak the raw id
													     into either. Absent/unassigned comes through as 0
													     from the proc, which matches no signed-in user. -->
													<xsl:attribute name="data-assigneduserid">
														<xsl:value-of select="normalize-space(./fkUserIDAssignedTo)"/>
													</xsl:attribute>
													<xsl:for-each select="./*">
														<xsl:choose>
															<!-- keys + client-level columns: see the header loop -->
															<xsl:when test="contains($hiddenCols, concat('|', name(), '|'))
																			 or contains($clientCols, concat('|', name(), '|'))">
																<td style="display: none;"/>
															</xsl:when>

															<!-- Case # opens the case inside ClientSpace -->
															<xsl:when test="name()='CaseNumber'">
																<td class="cs-casenum">
																	<xsl:choose>
																		<xsl:when test="number(../pkCaseID) &gt; 0">
																			<xsl:element name="a">
																				<xsl:attribute name="href">#</xsl:attribute>
																				<xsl:attribute name="data-url">
																					<xsl:value-of select="$caseRouteBase"/>
																					<xsl:text>/</xsl:text>
																					<xsl:choose>
																						<xsl:when test="number(../CaseProjTypeID) &gt; 0">
																							<xsl:value-of select="normalize-space(../CaseProjTypeID)"/>
																						</xsl:when>
																						<xsl:otherwise>
																							<xsl:value-of select="$caseProjTypeFallback"/>
																						</xsl:otherwise>
																					</xsl:choose>
																					<xsl:text>/</xsl:text>
																					<xsl:value-of select="normalize-space(../pkCaseID)"/>
																				</xsl:attribute>
																				<xsl:value-of select="."/>
																			</xsl:element>
																		</xsl:when>
																		<xsl:otherwise>
																			<span class="cs-mono">
																				<xsl:value-of select="."/>
																			</span>
																		</xsl:otherwise>
																	</xsl:choose>
																</td>
															</xsl:when>

															<xsl:when test="name()='CaseStatus' or name()='Status'">
																<!-- cs-statuscell: the view buttons read this cell
																     to decide which rows "My Cases" hides -->
																<td class="cs-statuscell">
																	<xsl:call-template name="chip">
																		<xsl:with-param name="value" select="."/>
																		<xsl:with-param name="scale" select="'status'"/>
																	</xsl:call-template>
																</td>
															</xsl:when>

															<xsl:when test="name()='Priority'">
																<!-- cs-prioritycell: the High Priority KPI tile
																     reads this cell when filtering -->
																<td class="cs-prioritycell">
																	<xsl:call-template name="chip">
																		<xsl:with-param name="value" select="."/>
																		<xsl:with-param name="scale" select="'priority'"/>
																	</xsl:call-template>
																</td>
															</xsl:when>

															<xsl:when test="name()='Subject'">
																<td class="cs-subject">
																	<xsl:call-template name="text-or-dash"/>
																</td>
															</xsl:when>

															<xsl:when test="name()='Issue' or name()='Description' or name()='Resolution'">
																<td class="cs-longtext">
																	<xsl:call-template name="longtext"/>
																</td>
															</xsl:when>

															<xsl:when test="name()='DueDate'">
																<td class="cs-mono cs-due">
																	<xsl:call-template name="date-or-dash"/>
																</td>
															</xsl:when>

															<xsl:when test="name()='CreateDate'">
																<td class="cs-mono">
																	<xsl:call-template name="date-or-dash"/>
																</td>
															</xsl:when>

															<xsl:when test="name()='CreateTime'">
																<td class="cs-mono cs-time">
																	<xsl:call-template name="text-or-dash"/>
																</td>
															</xsl:when>

															<xsl:otherwise>
																<td>
																	<xsl:call-template name="text-or-dash"/>
																</td>
															</xsl:otherwise>
														</xsl:choose>
													</xsl:for-each>
												</tr>
											</xsl:for-each>
										</tbody>
									</table>
								</div>
								<br/>
							</xsl:for-each>
						</xsl:when>

						<!-- Nothing came back from the stored procedure at all. Without this
						     branch the entire widget body sits inside the for-each above and
						     the tile renders blank, which reads as a broken widget. Show the
						     KPI strip zeroed out plus the empty-state row so the widget still
						     frames itself and says the filters matched no cases. The client
						     context card is skipped on purpose: with no rows there is no
						     client to name, and the column headers are skipped because the
						     column set is discovered from row[1]. -->
						<xsl:otherwise>
							<xsl:call-template name="kpi-strip"/>
							<div class="cs-tablewrap">
								<xsl:call-template name="case-toolbar"/>
								<table class="cs-sortable">
									<tbody>
										<tr>
											<td class="cs-empty" colspan="100%">
											<span class="cs-empty-ico">
												<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="stroke: var(--green600);">
													<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
													<polyline points="22 4 12 14.01 9 11.27"/>
												</svg>
											</span>
											<div class="cs-empty-title">No open cases</div>
										</td>
										</tr>
									</tbody>
								</table>
							</div>
							<br/>
						</xsl:otherwise>
					</xsl:choose>
				</div>

				<!-- Flags past-due dates on cases that are not already green/closed,
				     fills the "Past Due" tile, and fits the table to the widget
				     viewport so scrollbars appear when the tile is small. Written
				     without the "<" character so it is safe inside XSLT without
				     CDATA. Fails silently. -->

				<script type="text/javascript">
					/* cs-cases-widget-script (marker: csExpandTab finds this
					   script tag in the DOM to carry it into the new tab) */
					(function () {
					/* Snapshot of the widget's untouched markup, taken before
					   any wiring or view-building mutates the DOM. The expand
					   button writes this into a fresh tab so the widget can
					   boot there cleanly on its own. */
					var PRISTINE_WIDGET = '';
					try {
					var pw = document.querySelector('.cs-specific-widget');
					if (pw) { PRISTINE_WIDGET = pw.outerHTML; }
					} catch (ePr) { }
					function parseDate(s) {
					var m = /^(\d{4})-(\d{2})-(\d{2})/.exec(s);
					if (m) { return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])); }
					m = /^(\d{1,2})\/(\d{1,2})\/(\d{4})/.exec(s);
					if (m) { return new Date(Number(m[3]), Number(m[1]) - 1, Number(m[2])); }
					var d = new Date(s);
					if (isNaN(d.getTime())) { return null; }
					d.setHours(0, 0, 0, 0);
					return d;
					}
					function markOverdue() {
					try {
					var today = new Date();
					today.setHours(0, 0, 0, 0);
					var weekMs = 7 * 86400000;
					var cells = document.querySelectorAll('.cs-specific-widget td.cs-due');
					var late = 0;
					var soon = 0;
					Array.prototype.slice.call(cells).forEach(function (cell) {
					var txt = (cell.textContent || '').replace(/^\s+|\s+$/g, '');
					if (!txt) { return; }
					var row = cell.parentNode;
					if (row.querySelector('.cs-chip-green')) { return; }
					var due = parseDate(txt);
					if (!due) { return; }
					if (today.getTime() > due.getTime()) {
					cell.className = cell.className + ' cs-overdue';
					late = late + 1;
					if (!cell.querySelector('.cs-duebadge')) {
					var lateDays = Math.round((today.getTime() - due.getTime()) / 86400000);
					var lb = document.createElement('span');
					lb.className = 'cs-duebadge';
					lb.textContent = lateDays + 'd late';
					cell.appendChild(lb);
					}
					} else {
					/* Not yet late: flag anything coming due within 7 days
					   (today included) for the Due This Week tile. */
					var diff = due.getTime() - today.getTime();
					if (weekMs >= diff) {
					cell.className = cell.className + ' cs-duesoon';
					soon = soon + 1;
					if (!cell.querySelector('.cs-duebadge')) {
					var soonDays = Math.round(diff / 86400000);
					var sb = document.createElement('span');
					sb.className = 'cs-duebadge';
					sb.textContent = (soonDays === 0) ? 'due today' : ('due in ' + soonDays + 'd');
					cell.appendChild(sb);
					}
					}
					}
					});
					var tiles = document.querySelectorAll('.cs-specific-widget .csOverdueCount');
					Array.prototype.slice.call(tiles).forEach(function (t) { t.textContent = late; });
					var soonTiles = document.querySelectorAll('.cs-specific-widget .csDueSoonCount');
					Array.prototype.slice.call(soonTiles).forEach(function (t) { t.textContent = soon; });
					} catch (e) { }
					}
					/* Fit the table to whatever height the widget viewport
					   actually has (an iframe'd widget sees its own tile
					   height as window.innerHeight). The table then scrolls
					   inside itself - vertically when rows overflow the
					   remaining space, horizontally when columns overflow
					   the tile width - instead of assuming 640px. */
					function sizeTableWrap() {
					try {
					var minH = 140;
					var wraps = document.querySelectorAll('.cs-specific-widget .cs-tablewrap');
					Array.prototype.slice.call(wraps).forEach(function (wrap) {
					var avail = window.innerHeight - wrap.getBoundingClientRect().top - 12;
					if (avail >= minH) { wrap.style.maxHeight = avail + 'px'; }
					else { wrap.style.maxHeight = minH + 'px'; }
					});
					} catch (e) { }
					}
					/* View buttons: "My Cases" narrows the grid to the signed-in
					   user's unfinished cases - it hides rows assigned to anybody
					   else (data-assigneduserid, stamped from the proc's hidden
					   fkUserIDAssignedTo column) and rows whose status means the
					   work is done. It is a toggle: while the filter is on the
					   button relabels itself "All Cases", so clicking it a second
					   time reads as what it does - release the filter and restore
					   every row. 'all' clears the filter outright. Global because
					   the toolbar buttons call it via onclick. Keep
					   CLOSED_STATUSES in sync with $notOpenStatuses above. */
					var CLOSED_STATUSES = ['closed', 'resolved', 'complete', 'completed', 'done', 'cancelled', 'canceled', 'void'];
					/* Set by the ClientSpace layout (_Layout.cshtml). The widget is
					   rendered into the host page, so it is in scope here. If the
					   host ever fails to publish it we deliberately fall back to
					   filtering on status alone - a null id matches no row, which
					   would otherwise blank the whole grid. */
					function currentUserID() {
					var id = parseInt(window.cs_hid_currentUserID, 10);
					return isNaN(id) ? null : id;
					}
					window.csCaseMode = function (btn, mode) {
					try {
					/* Toggle: clicking a filter button that is already pressed
					   clears the filter and shows every row again. Read the
					   pressed state before it gets reset below. An explicit
					   'all' always clears, so it stays idempotent if a
					   show-everything button is ever added back. */
					var wasActive = /bt-active/.test(btn.className);
					var effective = mode;
					if (wasActive) {
					if (mode !== 'all') { effective = 'all'; }
					}
					/* Reset every view button to its released look and its
					   released label/tooltip (data-label-off / data-title-off,
					   both stamped on the button by the toolbar template so the
					   wording lives with the markup, not in here). */
					var btns = document.querySelectorAll('.cs-specific-widget .cs-viewbtn');
					Array.prototype.slice.call(btns).forEach(function (b) {
					b.className = b.className.replace(/\s*bt-active/g, '');
					var labelOff = b.getAttribute('data-label-off');
					if (labelOff) { b.textContent = labelOff; }
					var titleOff = b.getAttribute('data-title-off');
					if (titleOff) { b.title = titleOff; }
					});
					/* Filter is on: press the button and swap it to the label that
					   says what the next click will do - "All Cases". */
					if (effective !== 'all') {
					btn.className = btn.className + ' bt-active';
					var labelOn = btn.getAttribute('data-label-on');
					if (labelOn) { btn.textContent = labelOn; }
					var titleOn = btn.getAttribute('data-title-on');
					if (titleOn) { btn.title = titleOn; }
					}
					var me = currentUserID();
					var rows = document.querySelectorAll('.cs-specific-widget tbody tr');
					Array.prototype.slice.call(rows).forEach(function (row) {
					row.className = row.className.replace(/\s*cs-mode-hidden/g, '');
					if (effective === 'open') {
					var hide = false;
					/* Assigned to someone other than me. Nested ifs rather than
					   &amp;&amp; so this block stays free of characters that would
					   need escaping inside the stylesheet. */
					if (me !== null) {
					if (row.hasAttribute('data-assigneduserid')) {
					var owner = parseInt(row.getAttribute('data-assigneduserid'), 10);
					if (isNaN(owner)) { hide = true; }
					else if (owner !== me) { hide = true; }
					}
					}
					/* Already finished. */
					var cell = row.querySelector('td.cs-statuscell');
					if (cell) {
					var s = (cell.textContent || '').replace(/^\s+|\s+$/g, '').toLowerCase();
					if (CLOSED_STATUSES.indexOf(s) !== -1) { hide = true; }
					}
					if (hide) { row.className = row.className + ' cs-mode-hidden'; }
					}
					});
					refreshViews();
					try { btn.blur(); } catch (e2) { }
					} catch (e) { }
					};
					/* Export to CSV. Self-contained on purpose: the button used to
					   call table_to_csv() out of
					   /Next/custom/oas_custom_expcsvsortsrch.js, and clicking it
					   errored. Owning the few lines here means the button cannot
					   be broken by that shared file, and it lets the export match
					   what the grid is actually showing - the toolbar row, the
					   display:none key columns and any row hidden by the My Cases
					   filter are all left out. */
					function csvCell(text) {
					var s = (text === null || text === undefined) ? '' : String(text);
					s = s.replace(/\s+/g, ' ').replace(/^ | $/g, '');
					/* Blank cells render an em dash; export them as empty. */
					if (s === '\u2014') { s = ''; }
					if (/[",\r\n]/.test(s)) { s = '"' + s.replace(/"/g, '""') + '"'; }
					return s;
					}
					function closestTable(node) {
					while (node) {
					if (node.tagName) {
					if (node.tagName.toLowerCase() === 'table') { return node; }
					}
					node = node.parentNode;
					}
					return null;
					}
					window.csExportCases = function (btn) {
					try {
					/* The toolbar sits outside the table now, so walk to the
					   shared scroll wrap and find the grid inside it. */
					var table = closestTable(btn);
					if (!table) {
					var n = btn;
					while (n) {
					if (n.className) {
					if (/cs-tablewrap/.test(String(n.className))) { break; }
					}
					n = n.parentNode;
					}
					if (n) { table = n.querySelector('table.cs-sortable'); }
					}
					if (!table) { return; }
					var lines = [];
					var rows = table.querySelectorAll('tr');
					Array.prototype.slice.call(rows).forEach(function (row) {
					/* Toolbar chrome, rows the filter is hiding, and the
					   no-results placeholder are not data. */
					if (/cs-toolbar-row/.test(row.className)) { return; }
					if (/cs-mode-hidden/.test(row.className)) { return; }
					if (/cs-search-hidden/.test(row.className)) { return; }
					if (/cs-kpi-hidden/.test(row.className)) { return; }
					if (/cs-colfilter-hidden/.test(row.className)) { return; }
					if (/cs-grouprow/.test(row.className)) { return; }
					if (/cs-expandrow/.test(row.className)) { return; }
					if (row.querySelector('.cs-empty')) { return; }
					var out = [];
					var cells = row.querySelectorAll('th, td');
					Array.prototype.slice.call(cells).forEach(function (cell) {
					if (cell.style) {
					if (cell.style.display === 'none') { return; }
					}
					/* Strip relative-time badges so the Due column exports as
					   the bare date. */
					var clone = cell.cloneNode(true);
					Array.prototype.slice.call(clone.querySelectorAll('.cs-duebadge')).forEach(function (b) {
					b.parentNode.removeChild(b);
					});
					out.push(csvCell(clone.textContent));
					});
					if (out.length) { lines.push(out.join(',')); }
					});
					if (!lines.length) { return; }
					/* Leading BOM so Excel reads it as UTF-8. */
					var csv = '\ufeff' + lines.join('\r\n') + '\r\n';
					var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
					var name = 'MyCases.csv';
					if (navigator.msSaveBlob) { navigator.msSaveBlob(blob, name); return; }
					var link = document.createElement('a');
					link.href = URL.createObjectURL(blob);
					link.download = name;
					link.style.display = 'none';
					document.body.appendChild(link);
					link.click();
					document.body.removeChild(link);
					setTimeout(function () { URL.revokeObjectURL(link.href); }, 2000);
					try { btn.blur(); } catch (e2) { }
					} catch (e) { }
					};
					/* Click-to-sort. Self-owned like search and export. Clicking a
					   header sorts by that column ascending; clicking the same
					   header again flips the direction. The th-sort-asc/desc
					   classes paint the arrow and the highlight. A column whose
					   values all look like dates, numbers, or priority names is
					   compared that way (so Priority sorts High before Medium
					   before Low, not alphabetically); anything else compares as
					   case-insensitive text. Blank cells always sink to the
					   bottom, whichever direction is active. */
					var PRIORITY_RANKS = {
					'high': 1, 'critical': 1, 'urgent': 1, 'emergency': 1, '1': 1, '1 - high': 1, 'p1': 1,
					'medium': 2, 'normal': 2, 'standard': 2, 'moderate': 2, '2': 2, '2 - medium': 2, 'p2': 2,
					'low': 3, 'minor': 3, '3': 3, '3 - low': 3, 'p3': 3
					};
					function sortKeyDate(s) {
					var m = /^(\d{4})-(\d{2})-(\d{2})/.exec(s);
					if (m) { return (new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]))).getTime(); }
					/* Prefix match (no $ anchor): a due cell's text may carry a
					   relative-time badge after the date. */
					m = /^(\d{1,2})\/(\d{1,2})\/(\d{4})/.exec(s);
					if (m) { return (new Date(Number(m[3]), Number(m[1]) - 1, Number(m[2]))).getTime(); }
					return null;
					}
					window.csSortByHeader = function (th) {
					try {
					var table = closestTable(th);
					if (!table) { return; }
					var body = table.querySelector('tbody');
					if (!body) { return; }
					var idx = Array.prototype.indexOf.call(th.parentNode.children, th);
					if (idx === -1) { return; }
					var dir = 1;
					if (/th-sort-asc/.test(th.className)) { dir = -1; }
					var heads = th.parentNode.querySelectorAll('th');
					Array.prototype.slice.call(heads).forEach(function (h) {
					h.className = h.className.replace(/\s*th-sort-(asc|desc)/g, '');
					});
					th.className = th.className + (dir === 1 ? ' th-sort-asc' : ' th-sort-desc');
					var rows = [];
					Array.prototype.slice.call(body.querySelectorAll('tr')).forEach(function (r) {
					if (r.querySelector('.cs-empty')) { return; }
					if (/cs-grouprow|cs-expandrow/.test(r.className)) { return; }
					rows.push(r);
					});
					var entries = rows.map(function (r) {
					var cell = r.children[idx];
					var text = cell ? (cell.textContent || '') : '';
					text = text.replace(/^\s+|\s+$/g, '');
					if (text === '\u2014') { text = ''; }
					return { row: r, text: text };
					});
					/* Decide the column type from its non-empty values. */
					var allDate = true, allNum = true, allPri = true, seen = 0;
					entries.forEach(function (e) {
					if (e.text === '') { return; }
					seen = seen + 1;
					if (sortKeyDate(e.text) === null) { allDate = false; }
					if (!/^-?\d+(\.\d+)?$/.test(e.text)) { allNum = false; }
					if (PRIORITY_RANKS[e.text.toLowerCase()] === undefined) { allPri = false; }
					});
					if (seen === 0) { allDate = false; allNum = false; allPri = false; }
					entries.forEach(function (e) {
					if (e.text === '') { e.key = null; }
					else if (allDate) { e.key = sortKeyDate(e.text); }
					else if (allNum) { e.key = parseFloat(e.text); }
					else if (allPri) { e.key = PRIORITY_RANKS[e.text.toLowerCase()]; }
					else { e.key = e.text.toLowerCase(); }
					});
					entries.sort(function (a, b) {
					if (a.key === null) { return (b.key === null) ? 0 : 1; }
					if (b.key === null) { return -1; }
					if (a.key > b.key) { return dir; }
					if (b.key > a.key) { return 0 - dir; }
					return 0;
					});
					entries.forEach(function (e) { body.appendChild(e.row); });
					refreshViews();
					} catch (e) { }
					};
					function wireSorting() {
					try {
					var heads = document.querySelectorAll('.cs-specific-widget th.sticky-header');
					Array.prototype.slice.call(heads).forEach(function (th) {
					if (th.getAttribute('data-sort-wired') === '1') { return; }
					th.setAttribute('data-sort-wired', '1');
					th.addEventListener('click', function () { window.csSortByHeader(th); });
					});
					} catch (e) { }
					}
					/* ============ Shared view state ============
					   The KPI tile filter, the group-by choice, and board mode
					   all sit on top of the same row pool. Anything that changes
					   which rows are visible ends by calling refreshViews() so
					   the group counts and board cards stay in step. */
					var kpiFilter = null;
					var groupBy = '';
					/* Column filters: index of the column -> lowercase text it
					   must contain. Active only while the Filter button is on. */
					var filterOn = false;
					var colFilters = {};
					var boardMode = false;
					var cardsMode = false;
					var cardsIndex = 0;
					/* On phones the case list starts folded so the card is the
					   first thing you see; the toggle bar (mobile-only) flips
					   it and the choice is saved with the other preferences.
					   null = no explicit choice yet: fall back to "folded on
					   narrow screens", checked live so it tracks the same
					   breakpoint the CSS uses. */
					var listCollapsed = null;
					function effectiveListCollapsed() {
					if (listCollapsed === true) { return true; }
					if (listCollapsed === false) { return false; }
					try { return window.matchMedia('(max-width: 640px)').matches; } catch (e) { return false; }
					}
					/* Whether the carousel's Details section is unfolded. One
					   global switch, not per-card: open it once and it stays
					   open as you flip, and across visits. */
					var detailsOpen = false;
					var darkMode = false;
					var colIndex = {};
					/* Table groups folded by the user, keyed by group label -
					   the fold survives regrouping and filter changes. */
					var collapsedGroups = {};
					function buildColIndex() {
					try {
					colIndex = {};
					var heads = document.querySelectorAll('.cs-specific-widget .cs-sortable thead tr th');
					Array.prototype.slice.call(heads).forEach(function (th) {
					var label = (th.textContent || '').replace(/^\s+|\s+$/g, '');
					if (label === '') { return; }
					if (colIndex[label] === undefined) {
					colIndex[label] = Array.prototype.indexOf.call(th.parentNode.children, th);
					}
					});
					} catch (e) { }
					}
					function dataRows() {
					var out = [];
					try {
					var rows = document.querySelectorAll('.cs-specific-widget .cs-sortable tbody tr');
					Array.prototype.slice.call(rows).forEach(function (r) {
					if (/cs-grouprow|cs-expandrow/.test(r.className)) { return; }
					if (r.querySelector('.cs-empty')) { return; }
					out.push(r);
					});
					} catch (e) { }
					return out;
					}
					function rowVisible(r) {
					return !/cs-search-hidden|cs-mode-hidden|cs-kpi-hidden|cs-colfilter-hidden/.test(r.className);
					}
					/* Status buckets for the KPI tiles and board columns. Keep
					   NEW/HOLD in sync with $blueStatuses/$holdStatuses above. */
					var NEW_STATUSES = ['new', 'submitted'];
					var HOLD_STATUSES = ['on hold', 'hold', 'deferred', 'pending', 'waiting', 'suspended'];
					function rowStatusText(r) {
					var cell = r.querySelector('td.cs-statuscell');
					if (!cell) { return ''; }
					return (cell.textContent || '').replace(/^\s+|\s+$/g, '').toLowerCase();
					}
					function rowStatusClosed(r) {
					return CLOSED_STATUSES.indexOf(rowStatusText(r)) !== -1;
					}
					/* KPI tiles as filters: click a tile to narrow the grid to
					   the rows that tile counts; click the pressed tile again
					   (or the Total tile) to clear. Composes with search and the
					   My Cases button - each keeps its own hiding class. */
					window.csKpiFilter = function (tile, kind) {
					try {
					var wasActive = /cs-kpi-active/.test(tile.className);
					var tiles = document.querySelectorAll('.cs-specific-widget .cs-kpi');
					Array.prototype.slice.call(tiles).forEach(function (t) {
					t.className = t.className.replace(/\s*cs-kpi-active/g, '');
					});
					kpiFilter = null;
					if (kind !== 'total') {
					if (!wasActive) {
					kpiFilter = kind;
					tile.className = tile.className + ' cs-kpi-active';
					}
					}
					dataRows().forEach(function (r) {
					r.className = r.className.replace(/\s*cs-kpi-hidden/g, '');
					if (kpiFilter === null) { return; }
					var hide = false;
					var s = rowStatusText(r);
					if (kpiFilter === 'new') { hide = NEW_STATUSES.indexOf(s) === -1; }
					else if (kpiFilter === 'hold') { hide = HOLD_STATUSES.indexOf(s) === -1; }
					else if (kpiFilter === 'late') { hide = !r.querySelector('td.cs-overdue'); }
					else if (kpiFilter === 'soon') { hide = !r.querySelector('td.cs-duesoon'); }
					else if (kpiFilter === 'prog') {
					/* In Progress = active and neither new nor on hold. */
					hide = CLOSED_STATUSES.indexOf(s) !== -1;
					if (!hide) { if (NEW_STATUSES.indexOf(s) !== -1) { hide = true; } }
					if (!hide) { if (HOLD_STATUSES.indexOf(s) !== -1) { hide = true; } }
					}
					if (hide) { r.className = r.className + ' cs-kpi-hidden'; }
					});
					refreshViews();
					} catch (e) { }
					};
					/* Group-by, in both views. Table: re-parents the rows under
					   one heading per distinct value of the chosen column,
					   alphabetically, with a live count of the rows currently
					   visible in each group (empty groups hide themselves).
					   Board: rebuildBoard swaps its status columns for one
					   column per distinct value. */
					window.csGroupBy = function (sel) {
					groupBy = sel.value || '';
					refreshViews();
					savePrefs();
					/* Release focus so the arrow keys go back to the grid -
					   a focused select keeps native arrow behavior (changing
					   its own selection) and the widget stays hands-off. */
					try { sel.blur(); } catch (e) { }
					};
					var GROUP_COLS = { 'category': 'Category', 'assignedto': 'Assigned To', 'status': 'Status', 'priority': 'Priority' };
					function rebuildGroups() {
					try {
					var body = document.querySelector('.cs-specific-widget .cs-sortable tbody');
					if (!body) { return; }
					Array.prototype.slice.call(body.querySelectorAll('tr.cs-grouprow')).forEach(function (r) {
					body.removeChild(r);
					});
					dataRows().forEach(function (r) {
					r.className = r.className.replace(/\s*cs-group-hidden/g, '');
					});
					if (groupBy === '') { return; }
					var idx = colIndex[GROUP_COLS[groupBy]];
					if (idx === undefined) { return; }
					var dash = String.fromCharCode(8212);
					var groups = {};
					var keys = [];
					dataRows().forEach(function (r) {
					var cell = r.children[idx];
					var k = cell ? (cell.textContent || '').replace(/^\s+|\s+$/g, '') : '';
					if (k === '') { k = '(blank)'; }
					if (k === dash) { k = '(blank)'; }
					if (groups[k] === undefined) { groups[k] = []; keys.push(k); }
					groups[k].push(r);
					});
					keys.sort(function (a, b) {
					var x = a.toLowerCase();
					var y = b.toLowerCase();
					if (x > y) { return 1; }
					if (y > x) { return -1; }
					return 0;
					});
					keys.forEach(function (k) {
					var vis = 0;
					groups[k].forEach(function (r) { if (rowVisible(r)) { vis = vis + 1; } });
					var tr = document.createElement('tr');
					tr.className = (vis === 0) ? 'cs-grouprow cs-group-empty' : 'cs-grouprow';
					if (collapsedGroups[k]) { tr.className = tr.className + ' cs-group-collapsed'; }
					tr.setAttribute('data-group', k);
					tr.title = 'Collapse or expand this group';
					var td = document.createElement('td');
					td.colSpan = 100;
					td.className = 'cs-grouphead';
					var lbl = document.createElement('span');
					lbl.textContent = k;
					var cnt = document.createElement('span');
					cnt.className = 'cs-grouphead-count';
					cnt.textContent = vis;
					td.appendChild(lbl);
					td.appendChild(cnt);
					tr.appendChild(td);
					body.appendChild(tr);
					groups[k].forEach(function (r) {
					if (collapsedGroups[k]) { r.className = r.className + ' cs-group-hidden'; }
					body.appendChild(r);
					});
					});
					} catch (e) { }
					}
					/* Board (kanban) view: New / In Progress / On Hold / Past
					   Due, one card per visible case. Read-only - a card links
					   into the case, but dragging cannot change its status.
					   Closed/resolved cases never appear on the board. A
					   past-due case lands in Past Due whatever its status;
					   otherwise unrecognized active statuses count as In
					   Progress, so nothing silently disappears. */
					var BOARD_COLS = [
					{ key: 'new',  label: 'New',         cls: 'cs-bcol-new' },
					{ key: 'prog', label: 'In Progress', cls: 'cs-bcol-prog' },
					{ key: 'hold', label: 'On Hold',     cls: 'cs-bcol-hold' },
					{ key: 'late', label: 'Past Due',    cls: 'cs-bcol-late' }
					];
					/* Button labels carry their shortcut letter underlined, so
					   the swap must rebuild the element, not set textContent. */
					function setBtnLabel(btn, letter, rest) {
					while (btn.firstChild) { btn.removeChild(btn.firstChild); }
					var u = document.createElement('u');
					u.textContent = letter;
					btn.appendChild(u);
					btn.appendChild(document.createTextNode(rest));
					}
					window.csToggleBoard = function (btn) {
					try {
					boardMode = !boardMode;
					cardsMode = false;
					cardsIndex = 0;
					if (boardMode) { setBtnLabel(btn, 'T', 'able'); }
					else { setBtnLabel(btn, 'B', 'oard'); }
					var sibC = document.querySelector('.cs-specific-widget .cs-cardsbtn');
					if (sibC) { setBtnLabel(sibC, 'C', 'ards'); }
					var root = document.querySelector('.cs-specific-widget');
					root.className = root.className.replace(/\s*cs-boardmode|\s*cs-cardsmode/g, '');
					if (boardMode) { root.className = root.className + ' cs-boardmode'; }
					clearKbdHighlights();
					refreshViews();
					savePrefs();
					try { btn.blur(); } catch (e2) { }
					} catch (e) { }
					};
					/* ---------- Expand: just the widget, in a new tab ----------
					   ClientSpace renders the widget inline in the dashboard,
					   so opening the page URL would drag the whole dashboard
					   along. Instead: write a fresh document containing only
					   the widget's pristine markup, its styles, and this
					   script, and let it boot there on its own. */
					function widgetStyles() {
					var out = [];
					var els = document.getElementsByTagName('style');
					var i;
					for (i = 0; els.length > i; i++) {
					var t = els[i].textContent || '';
					if (t.indexOf('cs-specific-widget') !== -1) { out.push(t); }
					}
					return out.join('\n');
					}
					function selfScriptText() {
					var els = document.getElementsByTagName('script');
					var i;
					for (i = 0; els.length > i; i++) {
					var t = els[i].textContent || '';
					if (t.indexOf('cs-cases-widget-script') !== -1) { return t; }
					}
					return '';
					}
					window.csExpandTab = function (btn) {
					try {
					var LT = String.fromCharCode(60);
					var w = window.open('', '_blank');
					if (!w) { return; }
					var parts = [];
					parts.push(LT + '!DOCTYPE html' + '>');
					parts.push(LT + 'html' + '>');
					parts.push(LT + 'head' + '>');
					parts.push(LT + 'meta charset="utf-8"/' + '>');
					parts.push(LT + 'meta name="viewport" content="width=device-width, initial-scale=1"/' + '>');
					parts.push(LT + 'title' + '>' + 'Cases' + LT + '/title' + '>');
					parts.push(LT + 'style' + '>' + widgetStyles() + LT + '/style' + '>');
					parts.push(LT + 'style' + '>' + 'body { margin: 14px; background: #f3f3f4; } body:has(.cs-dark) { background: #10151d; }' + LT + '/style' + '>');
					parts.push(LT + '/head' + '>');
					parts.push(LT + 'body' + '>');
					parts.push(PRISTINE_WIDGET);
					var js = selfScriptText();
					if (js !== '') {
					parts.push(LT + 'script' + '>' + js + LT + '/script' + '>');
					}
					parts.push(LT + '/body' + '>');
					parts.push(LT + '/html' + '>');
					w.document.open();
					w.document.write(parts.join('\n'));
					w.document.close();
					} catch (e) { }
					try { btn.blur(); } catch (e2) { }
					};
					window.csToggleDark = function (btn) {
					try {
					darkMode = !darkMode;
					var root = document.querySelector('.cs-specific-widget');
					root.className = root.className.replace(/\s*cs-dark/g, '');
					if (darkMode) { root.className = root.className + ' cs-dark'; }
					btn.textContent = String.fromCharCode(darkMode ? 9728 : 9790);
					btn.title = darkMode ? 'Switch to light mode' : 'Switch to dark mode';
					savePrefs();
					try { btn.blur(); } catch (e2) { }
					} catch (e) { }
					};
					window.csToggleCards = function (btn) {
					try {
					cardsMode = !cardsMode;
					boardMode = false;
					cardsIndex = 0;
					if (cardsMode) { setBtnLabel(btn, 'T', 'able'); }
					else { setBtnLabel(btn, 'C', 'ards'); }
					var sibB = document.querySelector('.cs-specific-widget .cs-boardbtn');
					if (sibB) { setBtnLabel(sibB, 'B', 'oard'); }
					var root = document.querySelector('.cs-specific-widget');
					root.className = root.className.replace(/\s*cs-boardmode|\s*cs-cardsmode/g, '');
					if (cardsMode) { root.className = root.className + ' cs-cardsmode'; }
					clearKbdHighlights();
					refreshViews();
					savePrefs();
					try { btn.blur(); } catch (e2) { }
					} catch (e) { }
					};
					function boardContainer() {
					var wrap = document.querySelector('.cs-specific-widget .cs-tablewrap');
					if (!wrap) { return null; }
					var board = wrap.querySelector('.cs-board');
					if (!board) {
					board = document.createElement('div');
					board.className = 'cs-board';
					wrap.appendChild(board);
					}
					return board;
					}
					function bucketFor(row) {
					var s = rowStatusText(row);
					if (CLOSED_STATUSES.indexOf(s) !== -1) { return null; }
					if (row.querySelector('td.cs-overdue')) { return 'late'; }
					if (NEW_STATUSES.indexOf(s) !== -1) { return 'new'; }
					if (HOLD_STATUSES.indexOf(s) !== -1) { return 'hold'; }
					return 'prog';
					}
					function cellText(row, label) {
					var idx = colIndex[label];
					if (idx === undefined) { return ''; }
					var cell = row.children[idx];
					if (!cell) { return ''; }
					/* Clone so the relative-time badge can be stripped without
					   touching the live cell. */
					var clone = cell.cloneNode(true);
					Array.prototype.slice.call(clone.querySelectorAll('.cs-duebadge')).forEach(function (b) {
					b.parentNode.removeChild(b);
					});
					var t = (clone.textContent || '').replace(/^\s+|\s+$/g, '');
					var dash = String.fromCharCode(8212);
					return (t === dash) ? '' : t;
					}
					function buildCard(row) {
					var card = document.createElement('div');
					card.className = 'cs-bcard';
					var top = document.createElement('div');
					var numCell = row.querySelector('td.cs-casenum');
					var srcLink = numCell ? numCell.querySelector('a') : null;
					if (srcLink) {
					var a = document.createElement('a');
					a.setAttribute('href', '#');
					a.setAttribute('data-url', srcLink.getAttribute('data-url'));
					a.textContent = srcLink.textContent;
					top.appendChild(a);
					} else {
					var num = document.createElement('span');
					num.textContent = numCell ? (numCell.textContent || '').replace(/^\s+|\s+$/g, '') : '';
					top.appendChild(num);
					}
					var pc = row.querySelector('td.cs-prioritycell .cs-chip');
					if (pc) {
					var chip = pc.cloneNode(true);
					chip.style.marginLeft = '8px';
					top.appendChild(chip);
					}
					card.appendChild(top);
					var subjText = cellText(row, 'Subject');
					if (subjText !== '') {
					var subj = document.createElement('div');
					subj.className = 'cs-bcard-subj';
					subj.textContent = subjText;
					card.appendChild(subj);
					}
					/* Category + Case Type ride as small chips under the
					   subject. */
					var tags = document.createElement('div');
					tags.className = 'cs-bcard-tags';
					var cat = cellText(row, 'Category');
					if (cat !== '') {
					var catChip = document.createElement('span');
					catChip.className = 'cs-chip cs-chip-gray';
					catChip.textContent = cat;
					tags.appendChild(catChip);
					}
					var ctype = cellText(row, 'Case Type');
					if (ctype !== '') {
					var typeChip = document.createElement('span');
					typeChip.className = 'cs-chip cs-chip-blue';
					typeChip.textContent = ctype;
					tags.appendChild(typeChip);
					}
					if (tags.firstChild) { card.appendChild(tags); }
					var meta = document.createElement('div');
					meta.className = 'cs-bcard-meta';
					var who = cellText(row, 'Assigned To');
					if (who !== '') {
					var w = document.createElement('span');
					w.textContent = who;
					meta.appendChild(w);
					}
					var emp = cellText(row, 'Employee');
					if (emp !== '') {
					var e2 = document.createElement('span');
					e2.textContent = emp;
					meta.appendChild(e2);
					}
					var due = cellText(row, 'Due');
					if (due !== '') {
					var d = document.createElement('span');
					d.textContent = 'Due ' + due;
					if (row.querySelector('td.cs-overdue')) { d.className = 'cs-bcard-due-late'; }
					meta.appendChild(d);
					}
					if (meta.firstChild) { card.appendChild(meta); }
					return card;
					}
					/* Initials for the avatar circles: first letter of the
					   first two words, ignoring any "(EE-4)" style suffix. */
					function csInitials(name) {
					var clean = String(name).replace(/\(.*\)/g, '').replace(/^\s+|\s+$/g, '');
					var parts = clean.split(/\s+/);
					var out = '';
					parts.forEach(function (p) {
					if (out.length >= 2) { return; }
					if (p !== '') { out = out + p.charAt(0); }
					});
					if (out === '') { out = '#'; }
					return out.toUpperCase();
					}
					/* The carousel's card, laid out like a helpdesk ticket:
					   avatar + employee name with the muted case number, the
					   priority pill on the right, a created line, "Sub :"
					   subject, and an Assignee / Status / Due footer. */
					function buildBigCard(row) {
					var card = document.createElement('div');
					card.className = 'cs-bigcard';
					if (detailsOpen) { card.className = card.className + ' cs-showmore'; }
					var bucket = bucketFor(row);
					var bkey = (bucket === null) ? 'done' : bucket;
					function chipFrom(label) {
					var idx = colIndex[label];
					if (idx === undefined) { return null; }
					var cell = row.children[idx];
					if (!cell) { return null; }
					var chip = cell.querySelector('.cs-chip');
					return chip ? chip.cloneNode(true) : null;
					}
					var head = document.createElement('div');
					head.className = 'cs-tcard-head';
					var empRaw = cellText(row, 'Employee');
					var empName = empRaw.replace(/\s*\(.*\)\s*$/, '');
					if (empName === '') { empName = cellText(row, 'Assigned To'); }
					var av = document.createElement('div');
					av.className = 'cs-tcard-avatar cs-av-' + bkey;
					av.textContent = csInitials(empName);
					head.appendChild(av);
					var hmain = document.createElement('div');
					hmain.className = 'cs-tcard-hmain';
					var nameline = document.createElement('div');
					nameline.className = 'cs-tcard-nameline';
					var nm = document.createElement('span');
					nm.className = 'cs-tcard-name';
					nm.textContent = (empName === '') ? 'Case' : empName;
					if (empRaw !== '') { nm.title = empRaw; }
					nameline.appendChild(nm);
					var numCell = row.querySelector('td.cs-casenum');
					var srcLink = numCell ? numCell.querySelector('a') : null;
					var caseno = document.createElement('span');
					caseno.className = 'cs-tcard-caseno';
					if (srcLink) {
					var a = document.createElement('a');
					a.setAttribute('href', '#');
					a.setAttribute('data-url', srcLink.getAttribute('data-url'));
					a.textContent = String.fromCharCode(8211) + ' #' + srcLink.textContent.replace(/^\s+|\s+$/g, '');
					a.title = 'Open this case';
					caseno.appendChild(a);
					} else {
					caseno.textContent = numCell ? ('#' + (numCell.textContent || '').replace(/^\s+|\s+$/g, '')) : '';
					}
					nameline.appendChild(caseno);
					var prChip = chipFrom('Priority');
					if (prChip) {
					prChip.className = prChip.className + ' cs-tcard-pill';
					nameline.appendChild(prChip);
					}
					hmain.appendChild(nameline);
					var created = cellText(row, 'Created');
					var ctime = cellText(row, 'Created Time');
					if (created !== '') {
					var tl = document.createElement('div');
					tl.className = 'cs-tcard-time';
					var tt = 'Created ' + created;
					if (ctime !== '') { tt = tt + ' ' + String.fromCharCode(183) + ' ' + ctime; }
					tl.textContent = tt;
					hmain.appendChild(tl);
					}
					head.appendChild(hmain);
					card.appendChild(head);
					var subjText = cellText(row, 'Subject');
					if (subjText !== '') {
					var subj = document.createElement('div');
					subj.className = 'cs-tcard-sub';
					subj.title = subjText;
					var slbl = document.createElement('span');
					slbl.className = 'cs-tcard-sublbl';
					slbl.textContent = 'Sub : ';
					var stxt = document.createElement('span');
					stxt.textContent = subjText;
					subj.appendChild(slbl);
					subj.appendChild(stxt);
					card.appendChild(subj);
					}
					var tags = document.createElement('div');
					tags.className = 'cs-bcard-tags';
					var cat = cellText(row, 'Category');
					if (cat !== '') {
					var catChip = document.createElement('span');
					catChip.className = 'cs-chip cs-chip-gray';
					catChip.textContent = cat;
					tags.appendChild(catChip);
					}
					var ctype = cellText(row, 'Case Type');
					if (ctype !== '') {
					var typeChip = document.createElement('span');
					typeChip.className = 'cs-chip cs-chip-blue';
					typeChip.textContent = ctype;
					tags.appendChild(typeChip);
					}
					if (tags.firstChild) { card.appendChild(tags); }
					/* Footer: Assignee / Status / Due, each a small label over
					   an icon + value, like the reference ticket card. */
					var foot = document.createElement('div');
					foot.className = 'cs-tcard-foot';
					function footCol(label) {
					var col = document.createElement('div');
					col.className = 'cs-tcard-col';
					var l = document.createElement('div');
					l.className = 'cs-tcard-lbl';
					l.textContent = label;
					col.appendChild(l);
					foot.appendChild(col);
					return col;
					}
					var who = cellText(row, 'Assigned To');
					if (who !== '') {
					var c1 = footCol('Assignee');
					var v1 = document.createElement('div');
					v1.className = 'cs-tcard-val';
					var mav = document.createElement('span');
					mav.className = 'cs-tcard-minav';
					mav.textContent = csInitials(who);
					v1.appendChild(mav);
					var wn = document.createElement('span');
					wn.textContent = who;
					v1.appendChild(wn);
					c1.appendChild(v1);
					}
					if (empRaw !== '') {
					var cE = footCol('Employee');
					var vE = document.createElement('div');
					vE.className = 'cs-tcard-val';
					var eav = document.createElement('span');
					eav.className = 'cs-tcard-minav';
					eav.textContent = csInitials(empRaw);
					vE.appendChild(eav);
					var en = document.createElement('span');
					en.textContent = empRaw;
					vE.appendChild(en);
					cE.appendChild(vE);
					}
					var statusText = cellText(row, 'Status');
					if (statusText !== '') {
					var c2 = footCol('Status');
					var v2 = document.createElement('div');
					v2.className = 'cs-tcard-val cs-tst-' + bkey;
					var sdot = document.createElement('span');
					sdot.className = 'cs-tcard-dot';
					v2.appendChild(sdot);
					var st = document.createElement('span');
					st.textContent = statusText;
					v2.appendChild(st);
					c2.appendChild(v2);
					}
					var due = cellText(row, 'Due');
					if (due !== '') {
					var c3 = footCol('Due Date');
					var v3 = document.createElement('div');
					v3.className = 'cs-tcard-val';
					var calWrap = document.createElement('span');
					calWrap.className = 'cs-tcard-cal';
					var svgNS3 = 'http://www.w3.org/2000/svg';
					var cal = document.createElementNS(svgNS3, 'svg');
					cal.setAttribute('width', '14');
					cal.setAttribute('height', '14');
					cal.setAttribute('viewBox', '0 0 24 24');
					cal.setAttribute('fill', 'none');
					cal.setAttribute('stroke', 'currentColor');
					cal.setAttribute('stroke-width', '2');
					cal.setAttribute('stroke-linecap', 'round');
					cal.setAttribute('stroke-linejoin', 'round');
					var crect = document.createElementNS(svgNS3, 'rect');
					crect.setAttribute('x', '3');
					crect.setAttribute('y', '4');
					crect.setAttribute('width', '18');
					crect.setAttribute('height', '18');
					crect.setAttribute('rx', '2');
					cal.appendChild(crect);
					[['16', '2', '16', '6'], ['8', '2', '8', '6'], ['3', '10', '21', '10']].forEach(function (pts) {
					var ln = document.createElementNS(svgNS3, 'line');
					ln.setAttribute('x1', pts[0]);
					ln.setAttribute('y1', pts[1]);
					ln.setAttribute('x2', pts[2]);
					ln.setAttribute('y2', pts[3]);
					cal.appendChild(ln);
					});
					calWrap.appendChild(cal);
					v3.appendChild(calWrap);
					var dv = document.createElement('span');
					dv.textContent = due;
					if (row.querySelector('td.cs-overdue')) {
					v3.className = v3.className + ' cs-tst-late';
					var badgeSrc = row.querySelector('td.cs-overdue .cs-duebadge');
					if (badgeSrc) {
					var badge = document.createElement('span');
					badge.className = 'cs-bigcard-badge';
					badge.textContent = badgeSrc.textContent;
					dv.appendChild(badge);
					}
					}
					v3.appendChild(dv);
					c3.appendChild(v3);
					}
					if (foot.firstChild) { card.appendChild(foot); }
					/* Fold-out details: same sections as the table's row
					   expansion. stopPropagation keeps the chevron click from
					   also being treated as a card click. */
					var more = document.createElement('button');
					more.type = 'button';
					more.className = 'cs-bigcard-more';
					more.title = 'Show or hide the full case details (down arrow opens, up arrow closes)';
					var moreLbl = document.createElement('span');
					moreLbl.textContent = 'Details';
					var chev = document.createElement('span');
					chev.className = 'cs-chevron';
					chev.textContent = String.fromCharCode(9662);
					more.appendChild(moreLbl);
					more.appendChild(chev);
					var details = document.createElement('div');
					details.className = 'cs-bigcard-details';
					['Issue', 'Description', 'Resolution'].forEach(function (label) {
					var t = cellText(row, label);
					if (t === '') { return; }
					var sec = document.createElement('div');
					sec.className = 'cs-tcard-sec';
					var h = document.createElement('div');
					h.className = 'cs-tcard-seclbl';
					h.textContent = label;
					var b = document.createElement('div');
					b.className = 'cs-tcard-sectxt';
					b.textContent = t;
					sec.appendChild(h);
					sec.appendChild(b);
					details.appendChild(sec);
					});
					if (!details.firstChild) {
					var m = document.createElement('div');
					m.className = 'cs-tcard-sec cs-muted';
					m.textContent = 'No additional details on this case.';
					details.appendChild(m);
					}
					more.onclick = function (ev) {
					ev.stopPropagation();
					if (/cs-showmore/.test(card.className)) {
					card.className = card.className.replace(/\s*cs-showmore/g, '');
					detailsOpen = false;
					} else {
					card.className = card.className + ' cs-showmore';
					detailsOpen = true;
					}
					savePrefs();
					try { more.blur(); } catch (e2) { }
					};
					card.appendChild(more);
					card.appendChild(details);
					return card;
					}
					/* Clicking a column header folds that column to a slim bar
					   (and unfolds it again). Collapsed state is keyed by the
					   column label so it survives the board rebuilding when
					   filters change. */
					var collapsedCols = {};
					function buildColumn(label, cls, rows) {
					var col = document.createElement('div');
					col.className = cls ? ('cs-bcol ' + cls) : 'cs-bcol';
					if (collapsedCols[label]) { col.className = col.className + ' cs-bcol-collapsed'; }
					var head = document.createElement('div');
					head.className = 'cs-bcol-head';
					head.title = 'Collapse or expand this column';
					var lbl = document.createElement('span');
					lbl.textContent = label;
					var cnt = document.createElement('span');
					cnt.className = 'cs-bcol-count';
					cnt.textContent = rows.length;
					head.appendChild(lbl);
					head.appendChild(cnt);
					head.addEventListener('click', function () {
					if (collapsedCols[label]) { delete collapsedCols[label]; }
					else { collapsedCols[label] = true; }
					rebuildBoard();
					savePrefs();
					});
					col.appendChild(head);
					rows.forEach(function (r) { col.appendChild(buildCard(r)); });
					return col;
					}
					function rebuildBoard() {
					try {
					if (!boardMode) { return; }
					var board = boardContainer();
					if (!board) { return; }
					while (board.firstChild) { board.removeChild(board.firstChild); }
					var note = document.createElement('div');
					note.className = 'cs-board-note';
					note.textContent = 'Read-only board: drag and drop is not available. Open a case to update its status.';
					board.appendChild(note);
					var eligible = [];
					dataRows().forEach(function (r) {
					if (!rowVisible(r)) { return; }
					if (bucketFor(r) === null) { return; }
					eligible.push(r);
					});
					/* Nothing to put on the board: show the same illustrated
					   empty state the table uses instead of four bare lanes. */
					if (eligible.length === 0) {
					var empty = document.createElement('div');
					empty.className = 'cs-board-empty';
					var ico = document.createElement('span');
					ico.className = 'cs-empty-ico';
					var svgNS2 = 'http://www.w3.org/2000/svg';
					var esvg = document.createElementNS(svgNS2, 'svg');
					esvg.setAttribute('width', '28');
					esvg.setAttribute('height', '28');
					esvg.setAttribute('viewBox', '0 0 24 24');
					esvg.setAttribute('fill', 'none');
					esvg.setAttribute('stroke-width', '2');
					esvg.setAttribute('stroke-linecap', 'round');
					esvg.setAttribute('stroke-linejoin', 'round');
					esvg.style.stroke = 'var(--green600)';
					var ep = document.createElementNS(svgNS2, 'path');
					ep.setAttribute('d', 'M22 11.08V12a10 10 0 1 1-5.93-9.14');
					var epl = document.createElementNS(svgNS2, 'polyline');
					epl.setAttribute('points', '22 4 12 14.01 9 11.27');
					esvg.appendChild(ep);
					esvg.appendChild(epl);
					ico.appendChild(esvg);
					var etitle = document.createElement('div');
					etitle.className = 'cs-empty-title';
					etitle.textContent = 'No open cases';
					empty.appendChild(ico);
					empty.appendChild(etitle);
					board.appendChild(empty);
					return;
					}
					if (groupBy === '') {
					BOARD_COLS.forEach(function (c) {
					var rows = [];
					eligible.forEach(function (r) { if (bucketFor(r) === c.key) { rows.push(r); } });
					board.appendChild(buildColumn(c.label, c.cls, rows));
					});
					} else {
					/* Grouped: one neutral column per distinct value of the
					   chosen field, busiest first. A field with dozens of
					   values (50 categories, 50 owners) would make the board
					   unreadable, so only the top columns render and the long
					   tail collapses into a single "Other" column. The table
					   view has no such cap - a long list of subheadings still
					   scans fine there. */
					var MAX_BOARD_COLS = 12;
					var idx = colIndex[GROUP_COLS[groupBy]];
					if (idx === undefined) { return; }
					var dash = String.fromCharCode(8212);
					var groups = {};
					var keys = [];
					eligible.forEach(function (r) {
					var cell = r.children[idx];
					var k = cell ? (cell.textContent || '').replace(/^\s+|\s+$/g, '') : '';
					if (k === '') { k = '(blank)'; }
					if (k === dash) { k = '(blank)'; }
					if (groups[k] === undefined) { groups[k] = []; keys.push(k); }
					groups[k].push(r);
					});
					keys.sort(function (a, b) {
					var d = groups[b].length - groups[a].length;
					if (d !== 0) { return d; }
					var x = a.toLowerCase();
					var y = b.toLowerCase();
					if (x > y) { return 1; }
					if (y > x) { return -1; }
					return 0;
					});
					if (keys.length > MAX_BOARD_COLS) {
					var shown = keys.slice(0, MAX_BOARD_COLS - 1);
					var rest = keys.slice(MAX_BOARD_COLS - 1);
					shown.forEach(function (k) {
					board.appendChild(buildColumn(k, '', groups[k]));
					});
					var restRows = [];
					rest.forEach(function (k) {
					groups[k].forEach(function (r) { restRows.push(r); });
					});
					board.appendChild(buildColumn('Other (' + rest.length + ' more values)', '', restRows));
					} else {
					keys.forEach(function (k) {
					board.appendChild(buildColumn(k, '', groups[k]));
					});
					}
					}
					} catch (e) { }
					}
					/* ============ Cards carousel view ============
					   One large card at a time, flip through with arrows or
					   keyboard. Respects all filters, grouping, sorting. */
					function cardsContainer() {
					var wrap = document.querySelector('.cs-specific-widget .cs-tablewrap');
					if (!wrap) { return null; }
					var cards = wrap.querySelector('.cs-cardswrap');
					if (!cards) {
					cards = document.createElement('div');
					cards.className = 'cs-cardswrap';
					wrap.appendChild(cards);
					}
					return cards;
					}
					function rebuildCards() {
					try {
					if (!cardsMode) { return; }
					var cards = cardsContainer();
					if (!cards) { return; }
					while (cards.firstChild) { cards.removeChild(cards.firstChild); }
					var eligible = [];
					dataRows().forEach(function (r) {
					if (!rowVisible(r)) { return; }
					eligible.push(r);
					});
					if (eligible.length === 0) {
					var empty = document.createElement('div');
					empty.className = 'cs-board-empty';
					var ico = document.createElement('span');
					ico.className = 'cs-empty-ico';
					var svgNS = 'http://www.w3.org/2000/svg';
					var esvg = document.createElementNS(svgNS, 'svg');
					esvg.setAttribute('width', '28');
					esvg.setAttribute('height', '28');
					esvg.setAttribute('viewBox', '0 0 24 24');
					esvg.setAttribute('fill', 'none');
					esvg.setAttribute('stroke-width', '2');
					esvg.setAttribute('stroke-linecap', 'round');
					esvg.setAttribute('stroke-linejoin', 'round');
					esvg.style.stroke = 'var(--green600)';
					var ep = document.createElementNS(svgNS, 'path');
					ep.setAttribute('d', 'M22 11.08V12a10 10 0 1 1-5.93-9.14');
					var epl = document.createElementNS(svgNS, 'polyline');
					epl.setAttribute('points', '22 4 12 14.01 9 11.27');
					esvg.appendChild(ep);
					esvg.appendChild(epl);
					ico.appendChild(esvg);
					var etitle = document.createElement('div');
					etitle.className = 'cs-empty-title';
					etitle.textContent = 'No open cases';
					empty.appendChild(ico);
					empty.appendChild(etitle);
					cards.appendChild(empty);
					return;
					}
					/* A rebuild can leave the saved index past the end of a
					   freshly filtered deck - clamp it, don't reset it. */
					if (cardsIndex >= eligible.length) { cardsIndex = eligible.length - 1; }
					if (cardsIndex === -1) { cardsIndex = 0; }
					var container = document.createElement('div');
					container.className = 'cs-card-container';
					var list = document.createElement('div');
					list.className = 'cs-cardlist';
					/* With grouping on, the list gets section headers with
					   counts. Rows arrive pre-ordered by group (rebuildGroups
					   reorders the tbody), so headers appear on value change. */
					var groupCounts = {};
					if (groupBy !== '') {
					eligible.forEach(function (r) {
					var g0 = cellText(r, GROUP_COLS[groupBy]);
					if (g0 === '') { g0 = '(blank)'; }
					groupCounts[g0] = (groupCounts[g0] === undefined) ? 1 : groupCounts[g0] + 1;
					});
					}
					var lastGroup = null;
					eligible.forEach(function (r, idx) {
					if (groupBy !== '') {
					var gname = cellText(r, GROUP_COLS[groupBy]);
					if (gname === '') { gname = '(blank)'; }
					if (gname !== lastGroup) {
					lastGroup = gname;
					var ghead = document.createElement('div');
					ghead.className = 'cs-cardlist-group';
					ghead.textContent = gname + ' (' + groupCounts[gname] + ')';
					list.appendChild(ghead);
					}
					}
					var card = buildBigCard(r);
					card.className = card.className + ' cs-card-single';
					if (idx === cardsIndex) { card.className = card.className + ' cs-card-active'; }
					/* Remember the group value so the counter can show which
					   group the current card belongs to. */
					if (groupBy !== '') {
					var gv = cellText(r, GROUP_COLS[groupBy]);
					if (gv !== '') { card.setAttribute('data-cardgroup', gv); }
					}
					container.appendChild(card);
					/* Matching list row: status dot, case #, subject. */
					var item = document.createElement('div');
					item.className = 'cs-cardlist-item';
					if (idx === cardsIndex) { item.className = item.className + ' cs-cardlist-active'; }
					var dot = document.createElement('span');
					var b = bucketFor(r);
					var dcls = 'cs-cardlist-dot';
					if (b === 'new') { dcls = dcls + ' cs-cardlist-dot-new'; }
					else if (b === 'hold') { dcls = dcls + ' cs-cardlist-dot-hold'; }
					else if (b === 'late') { dcls = dcls + ' cs-cardlist-dot-late'; }
					else if (b === 'prog') { dcls = dcls + ' cs-cardlist-dot-prog'; }
					else { dcls = dcls + ' cs-cardlist-dot-done'; }
					dot.className = dcls;
					var inum = document.createElement('span');
					inum.className = 'cs-cardlist-num';
					var numCell2 = r.querySelector('td.cs-casenum');
					inum.textContent = numCell2 ? (numCell2.textContent || '').replace(/^\s+|\s+$/g, '') : '';
					var isubj = document.createElement('span');
					isubj.className = 'cs-cardlist-subj';
					isubj.textContent = cellText(r, 'Subject');
					isubj.title = isubj.textContent;
					item.appendChild(dot);
					item.appendChild(inum);
					item.appendChild(isubj);
					item.onclick = function () {
					cardsGoTo(idx, (idx >= cardsIndex) ? 1 : -1);
					};
					list.appendChild(item);
					});
					/* Swipe left/right flips cards on touch screens. */
					var touchX = null;
					container.addEventListener('touchstart', function (tev) {
					if (tev.touches.length === 1) { touchX = tev.touches[0].clientX; }
					}, { passive: true });
					container.addEventListener('touchend', function (tev) {
					if (touchX === null) { return; }
					var dx = tev.changedTouches[0].clientX - touchX;
					touchX = null;
					if (dx > 40) { cardsNavigate(-1); }
					if (-40 > dx) { cardsNavigate(1); }
					}, { passive: true });
					var controls = document.createElement('div');
					controls.className = 'cs-card-controls';
					var prevBtn = document.createElement('button');
					prevBtn.className = 'cs-card-nav cs-card-prev';
					prevBtn.type = 'button';
					prevBtn.textContent = String.fromCharCode(8249) + ' Prev';
					prevBtn.title = 'Previous case (left arrow, or Shift+Tab)';
					prevBtn.onclick = function () { cardsNavigate(-1); };
					var counter = document.createElement('div');
					counter.className = 'cs-card-counter';
					var nextBtn = document.createElement('button');
					nextBtn.className = 'cs-card-nav cs-card-next';
					nextBtn.type = 'button';
					nextBtn.textContent = 'Next ' + String.fromCharCode(8250);
					nextBtn.title = 'Next case (right arrow, or Tab)';
					nextBtn.onclick = function () { cardsNavigate(1); };
					controls.appendChild(prevBtn);
					controls.appendChild(counter);
					controls.appendChild(nextBtn);
					/* Two panes: the list on the left, card + controls on the
					   right. On phones a toggle bar sits above the list so it
					   can fold away and leave the card front and center. */
					var body = document.createElement('div');
					body.className = 'cs-cardsbody';
					var lt = document.createElement('div');
					lt.className = 'cs-cardlist-toggle';
					lt.title = 'Show or hide the case list';
					var ltLbl = document.createElement('span');
					ltLbl.textContent = 'Case list';
					var ltCount = document.createElement('span');
					ltCount.className = 'cs-cardlist-toggle-count';
					ltCount.textContent = eligible.length;
					var ltChev = document.createElement('span');
					ltChev.className = 'cs-chevron';
					ltChev.textContent = String.fromCharCode(9662);
					lt.appendChild(ltLbl);
					lt.appendChild(ltCount);
					lt.appendChild(ltChev);
					lt.onclick = function () {
					listCollapsed = !effectiveListCollapsed();
					applyListCollapsed(cards);
					savePrefs();
					};
					body.appendChild(lt);
					var main = document.createElement('div');
					main.className = 'cs-cardmain';
					main.appendChild(container);
					main.appendChild(controls);
					body.appendChild(list);
					body.appendChild(main);
					cards.appendChild(body);
					applyListCollapsed(cards);
					updateCardCounter();
					} catch (e) { }
					}
					function updateCardCounter() {
					try {
					var cards = cardsContainer();
					if (!cards) { return; }
					var counter = cards.querySelector('.cs-card-counter');
					if (!counter) { return; }
					var singles = cards.querySelectorAll('.cs-card-single');
					var text = (cardsIndex + 1) + ' of ' + singles.length;
					var active = singles[cardsIndex];
					if (active) {
					var gv = active.getAttribute('data-cardgroup');
					if (gv) { text = text + ' ' + String.fromCharCode(183) + ' ' + gv; }
					}
					counter.textContent = text;
					/* Re-trigger the pop animation on every change. */
					counter.className = counter.className.replace(/\s*cs-pop/g, '');
					void counter.offsetWidth;
					counter.className = counter.className + ' cs-pop';
					} catch (e) { }
					}
					function cardsGoTo(i, dir) {
					try {
					var cards = cardsContainer();
					if (!cards) { return; }
					var singles = cards.querySelectorAll('.cs-card-single');
					if (singles.length === 0) { return; }
					if (singles[cardsIndex]) {
					singles[cardsIndex].className = singles[cardsIndex].className.replace(/\s*cs-card-active|\s*cs-card-in-next|\s*cs-card-in-prev/g, '');
					}
					cardsIndex = i;
					if (cardsIndex === -1) { cardsIndex = singles.length - 1; }
					if (cardsIndex >= singles.length) { cardsIndex = 0; }
					/* Slide in from the side we're flipping toward. A deck of
					   one has nowhere to flip, so it gets no animation. */
					var entry = (dir === -1) ? ' cs-card-in-prev' : ' cs-card-in-next';
					if (singles.length === 1) { entry = ''; }
					var cls = singles[cardsIndex].className.replace(/\s*cs-card-in-next|\s*cs-card-in-prev/g, '');
					singles[cardsIndex].className = cls + ' cs-card-active' + entry;
					/* The Details preference follows you from card to card. */
					if (detailsOpen) {
					if (!/cs-showmore/.test(singles[cardsIndex].className)) {
					singles[cardsIndex].className = singles[cardsIndex].className + ' cs-showmore';
					}
					} else {
					singles[cardsIndex].className = singles[cardsIndex].className.replace(/\s*cs-showmore/g, '');
					}
					var items = cards.querySelectorAll('.cs-cardlist-item');
					Array.prototype.slice.call(items).forEach(function (it) {
					it.className = it.className.replace(/\s*cs-cardlist-active/g, '');
					});
					if (items[cardsIndex]) {
					items[cardsIndex].className = items[cardsIndex].className + ' cs-cardlist-active';
					try { items[cardsIndex].scrollIntoView({ block: 'nearest' }); } catch (e3) { }
					}
					updateCardCounter();
					savePrefs();
					} catch (e) { }
					}
					function cardsNavigate(dir) {
					cardsGoTo(cardsIndex + dir, dir);
					}
					function applyListCollapsed(cards) {
					try {
					cards.className = cards.className.replace(/\s*cs-list-collapsed/g, '');
					if (effectiveListCollapsed()) { cards.className = cards.className + ' cs-list-collapsed'; }
					} catch (e) { }
					}
					/* Fold or unfold the active card's Details - the keyboard
					   twin of clicking the Details button, so it also updates
					   the saved preference. */
					function setCardDetails(open) {
					try {
					var cards = cardsContainer();
					if (!cards) { return; }
					var active = cards.querySelector('.cs-card-single.cs-card-active');
					if (!active) { return; }
					detailsOpen = open;
					active.className = active.className.replace(/\s*cs-showmore/g, '');
					if (open) { active.className = active.className + ' cs-showmore'; }
					savePrefs();
					} catch (e) { }
					}
					/* ============ Row expansion ============
					   Click anywhere on a row (not a link/button) to unfold the
					   full Issue / Description / Resolution below it. Filters,
					   sorting, and grouping collapse open expansions first so
					   the panel can never detach from its row. */
					function collapseExpansions() {
					try {
					Array.prototype.slice.call(document.querySelectorAll('.cs-specific-widget tr.cs-expandrow')).forEach(function (r) {
					r.parentNode.removeChild(r);
					});
					} catch (e) { }
					}
					function toggleExpand(row) {
					var next = row.nextSibling;
					while (next) {
					if (next.nodeType === 1) { break; }
					next = next.nextSibling;
					}
					if (next) {
					if (/cs-expandrow/.test(next.className || '')) {
					next.parentNode.removeChild(next);
					return;
					}
					}
					var tr = document.createElement('tr');
					tr.className = 'cs-expandrow';
					var td = document.createElement('td');
					td.colSpan = 100;
					td.className = 'cs-expandcell';
					['Issue', 'Description', 'Resolution'].forEach(function (label) {
					var t = cellText(row, label);
					if (t === '') { return; }
					var sec = document.createElement('div');
					sec.className = 'cs-expandsec';
					var h = document.createElement('div');
					h.className = 'cs-expandlbl';
					h.textContent = label;
					var b = document.createElement('div');
					b.className = 'cs-expandtxt';
					b.textContent = t;
					sec.appendChild(h);
					sec.appendChild(b);
					td.appendChild(sec);
					});
					if (!td.firstChild) {
					var m = document.createElement('div');
					m.className = 'cs-expandsec cs-muted';
					m.textContent = 'No additional details on this case.';
					td.appendChild(m);
					}
					tr.appendChild(td);
					row.parentNode.insertBefore(tr, row.nextSibling);
					}
					function wireRowExpansion() {
					try {
					var widgets = document.querySelectorAll('.cs-specific-widget');
					Array.prototype.slice.call(widgets).forEach(function (w) {
					if (w.getAttribute('data-expand-wired') === '1') { return; }
					w.setAttribute('data-expand-wired', '1');
					w.addEventListener('click', function (ev) {
					try {
					var node = ev.target;
					var row = null;
					while (node) {
					if (node.tagName) {
					var tg = node.tagName.toLowerCase();
					if (tg === 'a') { return; }
					if (tg === 'button') { return; }
					if (tg === 'input') { return; }
					if (tg === 'select') { return; }
					if (tg === 'th') { return; }
					if (node.getAttribute) {
					if (node.getAttribute('data-url')) { return; }
					}
					if (tg === 'tr') { row = node; break; }
					}
					if (node === w) { break; }
					node = node.parentNode;
					}
					if (!row) { return; }
					if (/cs-grouprow/.test(row.className)) {
					/* Group headers toggle their fold instead of expanding. */
					var g = row.getAttribute('data-group');
					if (g !== null) {
					if (collapsedGroups[g]) { delete collapsedGroups[g]; }
					else { collapsedGroups[g] = true; }
					rebuildGroups();
					savePrefs();
					}
					return;
					}
					if (/cs-toolbar-row|cs-filterrow/.test(row.className)) { return; }
					if (row.querySelector('.cs-empty')) { return; }
					if (/cs-expandrow/.test(row.className)) {
					row.parentNode.removeChild(row);
					return;
					}
					var tbody = row.parentNode;
					if (!tbody) { return; }
					if (tbody.tagName.toLowerCase() !== 'tbody') { return; }
					toggleExpand(row);
					} catch (e) { }
					});
					});
					} catch (e) { }
					}
					/* ============ Keyboard navigation ============
					   "/" jumps to search, arrow keys walk a highlight through
					   the visible rows, Enter opens the highlighted case,
					   Escape clears. Inactive while typing in a field or while
					   the board is showing. */
					/* Exactly one highlight can exist at a time, ever. Every
					   move starts by stripping the highlight class from every
					   row and card in the DOM (not from a remembered element,
					   which can go stale when the board rebuilds), then paints
					   the one new target. */
					function clearKbdHighlights() {
					try {
					Array.prototype.slice.call(document.querySelectorAll('.cs-specific-widget tr.cs-kbd')).forEach(function (r) {
					r.className = r.className.replace(/\s*cs-kbd(?!-card)/g, '');
					});
					Array.prototype.slice.call(document.querySelectorAll('.cs-specific-widget .cs-kbd-card')).forEach(function (c) {
					c.className = c.className.replace(/\s*cs-kbd-card/g, '');
					});
					} catch (e) { }
					}
					/* Last line of defense against a STALE copy of this script
					   still listening in the page (a widget refresh can leave
					   one behind): kbdIntended is the single element this run
					   wants highlighted, and the sweep - scheduled to run after
					   every keystroke, before the browser paints - deletes any
					   highlight that is not it, no matter which code added it. */
					var kbdIntended = null;
					function sweepKbd() {
					try {
					Array.prototype.slice.call(document.querySelectorAll('.cs-specific-widget tr.cs-kbd, .cs-specific-widget .cs-kbd-card')).forEach(function (el) {
					if (el === kbdIntended) { return; }
					el.className = String(el.className).replace(/\s*cs-kbd-card/g, '').replace(/\s*cs-kbd(?!-card)/g, '');
					});
					} catch (e) { }
					}
					function scheduleKbdSweep() {
					try {
					if (window.requestAnimationFrame) { window.requestAnimationFrame(sweepKbd); return; }
					setTimeout(sweepKbd, 0);
					} catch (e) { }
					}
					function wireKeyboardNav() {
					/* Guarded like the other wiring: a widget refresh that
					   re-runs this script must not stack a second document
					   listener, which is what made the arrows jump two rows
					   and leave orphaned highlights behind. */
					if (document.documentElement.getAttribute('data-cs-kbd-wired') === '1') { return; }
					document.documentElement.setAttribute('data-cs-kbd-wired', '1');
					/* Keep the highlight visible by scrolling ONLY the widget's
					   own scroll container. scrollIntoView scrolls every
					   ancestor - including the host ClientSpace page - which
					   made each keypress yank the whole tile around. The
					   sticky toolbar and column headers float over the top of
					   the scroll area, so the usable top edge sits below them. */
					/* The widget's table wrap is not always the thing that
					   scrolls - inside ClientSpace the tile's own panel often
					   is, and when the wrap fits inside it, adjusting
					   wrap.scrollTop does nothing (which is why arrowing up
					   never brought hidden rows back). Walk up from the
					   element to whichever ancestor actually has overflow to
					   scroll, and adjust that one. */
					/* Reveal the element within ONE scroll container. The sticky
					   toolbar and column headers float over the rows in
					   whichever container scrolls, so the usable top edge is
					   where the chrome's bottom edge actually sits right now -
					   measured live, and only honored when the chrome is
					   inside this scroller's viewport. */
					function revealWithin(el, scroller) {
					try {
					var isPage = false;
					if (scroller === document.scrollingElement) { isPage = true; }
					if (scroller === document.documentElement) { isPage = true; }
					if (scroller === document.body) { isPage = true; }
					var wr;
					if (isPage) { wr = { top: 0, bottom: window.innerHeight }; }
					else { wr = scroller.getBoundingClientRect(); }
					var topEdge = wr.top;
					var trow = document.querySelector('.cs-specific-widget .cs-toolbar-band');
					if (trow) {
					var tb = trow.getBoundingClientRect().bottom;
					if (tb > topEdge) {
					if (wr.bottom > tb) { topEdge = tb; }
					}
					}
					var hdr = document.querySelector('.cs-specific-widget th.sticky-header');
					if (hdr) {
					if (hdr.offsetHeight > 0) {
					var hb = hdr.getBoundingClientRect().bottom;
					if (hb > topEdge) {
					if (wr.bottom > hb) { topEdge = hb; }
					}
					}
					}
					var er = el.getBoundingClientRect();
					if (topEdge > er.top) {
					scroller.scrollTop = scroller.scrollTop - (topEdge - er.top) - 4;
					} else if (er.bottom > wr.bottom - 8) {
					scroller.scrollTop = scroller.scrollTop + (er.bottom - (wr.bottom - 8));
					}
					} catch (e) { }
					}
					/* Nested scrolling is real in ClientSpace: the widget's
					   wrap can scroll INSIDE the tile panel, which itself
					   scrolls inside the page. Reveal the highlight in every
					   scrollable ancestor, innermost outward, re-measuring at
					   each level after the previous adjustment. */
					function revealInWrap(el) {
					try {
					var n = el.parentNode;
					var hops = 0;
					while (n) {
					if (hops > 12) { break; }
					var isPage = false;
					if (n === document.body) { isPage = true; }
					if (n === document.documentElement) { isPage = true; }
					if (n.nodeType === 1) {
					if (n.scrollHeight > n.clientHeight + 2) {
					var scrollable = isPage;
					if (!scrollable) {
					var oy = '';
					try { oy = getComputedStyle(n).overflowY; } catch (e0) { }
					if (oy === 'auto') { scrollable = true; }
					if (oy === 'scroll') { scrollable = true; }
					if (oy === 'overlay') { scrollable = true; }
					}
					if (scrollable) {
					var scroller = n;
					if (isPage) {
					if (document.scrollingElement) { scroller = document.scrollingElement; }
					}
					revealWithin(el, scroller);
					hops = hops + 1;
					}
					}
					}
					if (isPage) { break; }
					n = n.parentNode;
					}
					} catch (e) { }
					}
					function setKbd(row) {
					clearKbdHighlights();
					kbdIntended = row;
					if (row) {
					row.className = row.className + ' cs-kbd';
					revealInWrap(row);
					}
					scheduleKbdSweep();
					}
					function setKbdCard(card) {
					clearKbdHighlights();
					kbdIntended = card;
					if (card) {
					card.className = card.className + ' cs-kbd-card';
					revealInWrap(card);
					}
					scheduleKbdSweep();
					}
					function visibleList() {
					var out = [];
					dataRows().forEach(function (r) {
					if (!rowVisible(r)) { return; }
					/* Skip rows folded away inside a collapsed group (or
					   otherwise not rendered) - the arrows walk what the
					   user can actually see. */
					if (r.offsetParent === null) { return; }
					out.push(r);
					});
					return out;
					}
					var lastKeyName = '';
					var lastKeyStamp = 0;
					document.addEventListener('keydown', function (ev) {
					try {
					/* Some hosts re-dispatch keyboard events; a synthetic
					   duplicate ARRIVES within a few milliseconds of the real
					   one, while genuine key auto-repeat is 30ms+ apart. Drop
					   the duplicate so one press can never step twice. The
					   arrival time is measured on our own clock (Date.now) -
					   never ev.timeStamp, whose clock differs between native
					   and framework-synthesized events; one foreign stamp
					   would poison the gap math and silently swallow every
					   real keypress after it. */
					var stamp = Date.now();
					if (ev.key === lastKeyName) {
					var gap = stamp - lastKeyStamp;
					if (gap >= 0) {
					if (20 > gap) { return; }
					}
					}
					lastKeyName = ev.key;
					lastKeyStamp = stamp;
					var tag = document.activeElement ? document.activeElement.tagName.toLowerCase() : '';
					var typing = false;
					if (tag === 'input') { typing = true; }
					if (tag === 'select') { typing = true; }
					if (tag === 'textarea') { typing = true; }
					if (ev.key === '/') {
					if (!typing) {
					ev.preventDefault();
					ev.stopPropagation();
					var box = document.querySelector('.cs-specific-widget .searchBox');
					if (box) { box.focus(); }
					}
					return;
					}
					if (typing) { return; }
					/* View shortcuts: bare B / C / T jump straight to the
					   board, cards, or table, E opens the widget in its own
					   tab. Modifier combos (Ctrl+C copy etc.) pass through
					   untouched, and the buttons are clicked rather than the
					   modes set so their labels stay in sync. */
					if (!ev.ctrlKey) {
					if (!ev.altKey) {
					if (!ev.metaKey) {
					var vk = ev.key ? String(ev.key).toLowerCase() : '';
					if (vk === 'b') {
					ev.preventDefault();
					ev.stopPropagation();
					if (!boardMode) {
					var kbB = document.querySelector('.cs-specific-widget .cs-boardbtn');
					if (kbB) { kbB.click(); }
					}
					return;
					}
					if (vk === 'c') {
					ev.preventDefault();
					ev.stopPropagation();
					if (!cardsMode) {
					var kbC = document.querySelector('.cs-specific-widget .cs-cardsbtn');
					if (kbC) { kbC.click(); }
					}
					return;
					}
					if (vk === 't') {
					ev.preventDefault();
					ev.stopPropagation();
					if (boardMode) {
					var kbT = document.querySelector('.cs-specific-widget .cs-boardbtn');
					if (kbT) { kbT.click(); }
					} else if (cardsMode) {
					var kbT2 = document.querySelector('.cs-specific-widget .cs-cardsbtn');
					if (kbT2) { kbT2.click(); }
					}
					return;
					}
					if (vk === 'e') {
					ev.preventDefault();
					ev.stopPropagation();
					var kbE = document.querySelector('.cs-specific-widget .cs-expandbtn');
					if (kbE) { kbE.click(); }
					return;
					}
					if (vk === 'f') {
					ev.preventDefault();
					ev.stopPropagation();
					if (filterOn) {
					window.csClearFilters(null);
					} else {
					showFilters();
					var kfTr = ensureFilterRow();
					var kfFirst = kfTr ? kfTr.querySelector('.cs-colfilter') : null;
					if (kfFirst) { try { kfFirst.focus(); } catch (e5) { } }
					}
					return;
					}
					}
					}
					}
					if (ev.key === 'Escape') {
					kbdIntended = null;
					clearKbdHighlights();
					scheduleKbdSweep();
					return;
					}
					if (boardMode) {
					/* Board: up/down walk the cards in a column, left/right
					   hop between columns (keeping roughly the same height),
					   Enter opens the highlighted card. Collapsed columns are
					   skipped. The board rebuilds on filter changes, so the
					   current card is re-found from the DOM each keystroke. */
					var cols = [];
					Array.prototype.slice.call(document.querySelectorAll('.cs-specific-widget .cs-bcol')).forEach(function (c) {
					if (/cs-bcol-collapsed/.test(c.className)) { return; }
					var cards = Array.prototype.slice.call(c.querySelectorAll('.cs-bcard'));
					if (cards.length) { cols.push(cards); }
					});
					if (!cols.length) { return; }
					var cur = document.querySelector('.cs-specific-widget .cs-bcard.cs-kbd-card');
					var ci = -1;
					var ri = -1;
					if (cur) {
					cols.forEach(function (cards, x) {
					var y = cards.indexOf(cur);
					if (y !== -1) { ci = x; ri = y; }
					});
					}
					if (ev.key === 'ArrowDown') {
					ev.preventDefault();
					ev.stopPropagation();
					if (ci === -1) { setKbdCard(cols[0][0]); }
					else { setKbdCard(cols[ci][Math.min(cols[ci].length - 1, ri + 1)]); }
					} else if (ev.key === 'ArrowUp') {
					ev.preventDefault();
					ev.stopPropagation();
					if (ci === -1) { setKbdCard(cols[0][0]); }
					else { setKbdCard(cols[ci][Math.max(0, ri - 1)]); }
					} else if (ev.key === 'ArrowRight') {
					ev.preventDefault();
					ev.stopPropagation();
					if (ci === -1) { setKbdCard(cols[0][0]); }
					else {
					var nx = Math.min(cols.length - 1, ci + 1);
					setKbdCard(cols[nx][Math.min(cols[nx].length - 1, Math.max(0, ri))]);
					}
					} else if (ev.key === 'ArrowLeft') {
					ev.preventDefault();
					ev.stopPropagation();
					if (ci === -1) { setKbdCard(cols[0][0]); }
					else {
					var pv = Math.max(0, ci - 1);
					setKbdCard(cols[pv][Math.min(cols[pv].length - 1, Math.max(0, ri))]);
					}
					} else if (ev.key === 'Enter') {
					if (cur) {
					var ca = cur.querySelector('a');
					if (ca) {
					ev.preventDefault();
					ev.stopPropagation();
					openRecord(ca.getAttribute('data-url'));
					}
					}
					}
					return;
					}
					if (cardsMode) {
					/* Cards: Tab / right flip forward (Shift+Tab / left flip
					   back), down unfolds Details and up folds it, Enter
					   opens the current card. */
					if (ev.key === 'Tab') {
					ev.preventDefault();
					ev.stopPropagation();
					cardsNavigate(ev.shiftKey ? -1 : 1);
					} else if (ev.key === 'ArrowRight') {
					ev.preventDefault();
					ev.stopPropagation();
					cardsNavigate(1);
					} else if (ev.key === 'ArrowLeft') {
					ev.preventDefault();
					ev.stopPropagation();
					cardsNavigate(-1);
					} else if (ev.key === 'ArrowDown') {
					ev.preventDefault();
					ev.stopPropagation();
					setCardDetails(true);
					} else if (ev.key === 'ArrowUp') {
					ev.preventDefault();
					ev.stopPropagation();
					setCardDetails(false);
					} else if (ev.key === 'Enter') {
					var cards = cardsContainer();
					if (cards) {
					var singles = cards.querySelectorAll('.cs-card-single');
					if (singles[cardsIndex]) {
					var link = singles[cardsIndex].querySelector('a');
					if (link) {
					ev.preventDefault();
					ev.stopPropagation();
					openRecord(link.getAttribute('data-url'));
					}
					}
					}
					}
					return;
					}
					var rows = visibleList();
					if (!rows.length) { return; }
					/* The current row comes from the DOM, never from a saved
					   reference, so filters and sorts can't leave the arrows
					   pointing at a row that is no longer there. */
					var curRow = document.querySelector('.cs-specific-widget tbody tr.cs-kbd');
					if (ev.key === 'ArrowDown') {
					ev.preventDefault();
					ev.stopPropagation();
					var i = rows.indexOf(curRow);
					setKbd(rows[Math.min(rows.length - 1, i + 1)]);
					} else if (ev.key === 'ArrowUp') {
					ev.preventDefault();
					ev.stopPropagation();
					var j = rows.indexOf(curRow);
					if (j === -1) { j = 1; }
					setKbd(rows[Math.max(0, j - 1)]);
					} else if (ev.key === 'Enter') {
					if (curRow) {
					var a = curRow.querySelector('td.cs-casenum a');
					if (a) {
					ev.preventDefault();
					ev.stopPropagation();
					openRecord(a.getAttribute('data-url'));
					}
					}
					}
					} catch (e) { }
					/* Capture phase: the widget sees the key BEFORE the host
					   page's own handlers, so ClientSpace cannot swallow Enter
					   (or the arrows) on its way down the tree. */
					}, true);
					}
					/* ============ Remembered preferences ============
					   View mode, grouping, and column widths persist in
					   localStorage so the widget reopens the way it was left.
					   Everything is guarded - if storage is unavailable the
					   widget simply starts fresh. */
					var PREFS_KEY = 'csCasesWidgetPrefs';
					function savePrefs() {
					try {
					var w = {};
					var heads = document.querySelectorAll('.cs-specific-widget th.sticky-header');
					Array.prototype.slice.call(heads).forEach(function (th) {
					if (th.style.width) {
					w[String(Array.prototype.indexOf.call(th.parentNode.children, th))] = th.style.width;
					}
					});
					var prefs = { board: boardMode, cards: cardsMode, cardsIdx: cardsIndex, details: detailsOpen, dark: darkMode, listCollapsed: listCollapsed, groupBy: groupBy, widths: w, collapsed: collapsedCols, collapsedGroups: collapsedGroups };
					window.localStorage.setItem(PREFS_KEY, JSON.stringify(prefs));
					} catch (e) { }
					}
					function restorePrefs() {
					/* Storage failures must not block the view default, so the
					   read is guarded separately from the apply. */
					var prefs = null;
					try {
					var raw = window.localStorage.getItem(PREFS_KEY);
					if (raw) { prefs = JSON.parse(raw); }
					} catch (e) { }
					try {
					if (prefs) {
					if (prefs.widths) {
					var headRows = document.querySelectorAll('.cs-specific-widget .cs-sortable thead tr');
					if (headRows.length > 1) {
					var hrow = headRows[1];
					Object.keys(prefs.widths).forEach(function (k) {
					var th = hrow.children[Number(k)];
					if (th) {
					th.style.width = prefs.widths[k];
					th.style.minWidth = prefs.widths[k];
					}
					});
					}
					}
					if (prefs.groupBy) {
					groupBy = prefs.groupBy;
					var sel = document.querySelector('.cs-specific-widget .cs-groupsel');
					if (sel) { sel.value = prefs.groupBy; }
					}
					if (prefs.collapsed) { collapsedCols = prefs.collapsed; }
					if (prefs.collapsedGroups) { collapsedGroups = prefs.collapsedGroups; }
					if (prefs.cards) { cardsMode = true; }
					if (typeof prefs.cardsIdx === 'number') { cardsIndex = prefs.cardsIdx; }
					if (prefs.details === true) { detailsOpen = true; }
					if (prefs.listCollapsed === true) { listCollapsed = true; }
					if (prefs.listCollapsed === false) { listCollapsed = false; }
					if (prefs.dark === true) {
					var db = document.querySelector('.cs-specific-widget .cs-darkbtn');
					if (db) { window.csToggleDark(db); }
					}
					}
					/* The board is the default view. Saved preferences can switch
					   to table or cards mode. */
					var viewMode = 'board';
					if (prefs) {
					if (prefs.board === false) { viewMode = 'table'; }
					if (prefs.cards === true) { viewMode = 'cards'; }
					}
					if (viewMode === 'table') {
					/* Stay in table (default is board) */
					} else if (viewMode === 'cards') {
					var cb = document.querySelector('.cs-specific-widget .cs-cardsbtn');
					if (cb) { window.csToggleCards(cb); return; }
					} else {
					var bb = document.querySelector('.cs-specific-widget .cs-boardbtn');
					if (bb) { window.csToggleBoard(bb); return; }
					}
					if (groupBy !== '') { refreshViews(); }
					} catch (e2) { }
					}
					function refreshViews() {
					collapseExpansions();
					rebuildGroups();
					rebuildBoard();
					rebuildCards();
					}
					/* Record links (Case #, client name). The dataform URL is in the
					   anchor's data-url attribute and the click is handled here.
					   The href stays '#' on purpose: a javascript: href is itself a
					   URL, so anything that re-encodes it - the host page, a
					   sanitizer, the shared sort/search/export helper - escapes the
					   quotes that wrap the inner URL and the browser ends up
					   navigating to an address with a stray %27 on the end. A data
					   attribute is never treated as a URI, so there is nothing to
					   escape and the URL arrives at navigation.go exactly as
					   rendered. */
					function openRecord(url) {
					if (!url) { return; }
					try {
					if (window.csApp) {
					if (csApp.navigation) {
					if (csApp.navigation.go) { csApp.navigation.go(url); return; }
					}
					}
					} catch (e) { }
					/* csApp not in scope (widget rendered outside the app shell,
					   e.g. the expanded standalone tab): resolve the app-relative
					   ~/ prefix to the /Next/ root and navigate the top window. */
					var u = url;
					if (u.indexOf('~/') === 0) { u = '/Next/' + u.slice(2); }
					try { window.top.location.href = u; } catch (e2) { window.location.href = u; }
					}
					function wireRecordLinks() {
					try {
					var widgets = document.querySelectorAll('.cs-specific-widget');
					Array.prototype.slice.call(widgets).forEach(function (w) {
					/* Delegated, and guarded so a widget refresh that re-runs
					   this script cannot double-bind and open two frames. */
					if (w.getAttribute('data-links-wired') === '1') { return; }
					w.setAttribute('data-links-wired', '1');
					w.addEventListener('click', function (ev) {
					var node = ev.target;
					var link = null;
					while (node) {
					if (node.getAttribute) {
					if (node.getAttribute('data-url')) { link = node; break; }
					}
					if (node === w) { break; }
					node = node.parentNode;
					}
					if (!link) { return; }
					ev.preventDefault();
					openRecord(link.getAttribute('data-url'));
					});
					});
					} catch (e) { }
					}
					/* Search. Self-owned for the same reason as the CSV export -
					   the shared filter() was unreliable. Matches the query as a
					   case-insensitive substring across every VISIBLE cell of the
					   row (so Case #, status, subject, names, dates all hit;
					   hidden key columns cannot). Uses its own css class so it
					   composes with the My Cases filter instead of fighting it. */
					window.csFilterCases = function (input) {
					try {
					var q = (input.value || '').replace(/^\s+|\s+$/g, '').toLowerCase();
					var rows = document.querySelectorAll('.cs-specific-widget tbody tr');
					Array.prototype.slice.call(rows).forEach(function (row) {
					row.className = row.className.replace(/\s*cs-search-hidden/g, '');
					if (q === '') { return; }
					if (row.querySelector('.cs-empty')) { return; }
					var hay = '';
					var cells = row.querySelectorAll('td');
					Array.prototype.slice.call(cells).forEach(function (cell) {
					if (cell.style) {
					if (cell.style.display === 'none') { return; }
					}
					hay = hay + ' ' + (cell.textContent || '');
					});
					if (hay.toLowerCase().indexOf(q) === -1) {
					row.className = row.className + ' cs-search-hidden';
					}
					});
					refreshViews();
					} catch (e) { }
					};
					/* ============ Column-level filtering ============
					   The Filter button reveals an input under each column
					   header; each input narrows the grid to rows whose cell
					   contains the text (case-insensitive). Filters compose
					   with each other AND with search / My Cases / KPI tiles
					   through their own hiding class. */
					function ensureFilterRow() {
					var thead = document.querySelector('.cs-specific-widget .cs-sortable thead');
					if (!thead) { return null; }
					var existing = thead.querySelector('tr.cs-filterrow');
					if (existing) { return existing; }
					var headRow = thead.querySelector('tr');
					if (!headRow) { return null; }
					var tr = document.createElement('tr');
					tr.className = 'cs-filterrow';
					tr.style.display = 'none';
					Array.prototype.slice.call(headRow.children).forEach(function (th, idx) {
					var td = document.createElement('td');
					var csDisp = '';
					try { csDisp = window.getComputedStyle(th).display; } catch (e2) { }
					if (csDisp === 'none') {
					td.style.display = 'none';
					} else {
					var inp = document.createElement('input');
					inp.type = 'text';
					inp.className = 'cs-colfilter';
					inp.placeholder = 'Filter';
					inp.title = 'Show only rows where this column contains the text';
					inp.onkeyup = function (kev) {
					/* Escape steps out of the box so the F/B/C/T shortcuts
					   work again. */
					if (kev) {
					if (kev.key === 'Escape') {
					try { inp.blur(); } catch (e4) { }
					return;
					}
					}
					var v = (inp.value || '').replace(/^\s+|\s+$/g, '').toLowerCase();
					if (v === '') { delete colFilters[idx]; }
					else { colFilters[idx] = v; }
					applyColFilters();
					};
					inp.onclick = function (ev) { ev.stopPropagation(); };
					var wrap2 = document.createElement('div');
					wrap2.className = 'cs-colfilter-wrap';
					wrap2.appendChild(inp);
					var x = document.createElement('span');
					x.className = 'cs-colfilter-x';
					x.textContent = String.fromCharCode(10005);
					x.title = 'Clear this column filter';
					x.onclick = function (ev) {
					ev.stopPropagation();
					inp.value = '';
					delete colFilters[idx];
					applyColFilters();
					try { inp.focus(); } catch (e5) { }
					};
					wrap2.appendChild(x);
					td.appendChild(wrap2);
					}
					tr.appendChild(td);
					});
					thead.appendChild(tr);
					return tr;
					}
					function applyColFilters() {
					try {
					dataRows().forEach(function (r) {
					r.className = r.className.replace(/\s*cs-colfilter-hidden/g, '');
					var hide = false;
					var k;
					for (k in colFilters) {
					if (colFilters.hasOwnProperty(k)) {
					var cell = r.children[Number(k)];
					var txt = cell ? (cell.textContent || '').toLowerCase() : '';
					if (txt.indexOf(colFilters[k]) === -1) { hide = true; }
					}
					}
					if (hide) { r.className = r.className + ' cs-colfilter-hidden'; }
					});
					/* Show the x on filled boxes, hide it on empty ones. */
					Array.prototype.slice.call(document.querySelectorAll('.cs-specific-widget .cs-colfilter-wrap')).forEach(function (wr2) {
					var wi = wr2.querySelector('.cs-colfilter');
					var wx = wr2.querySelector('.cs-colfilter-x');
					if (!wi) { return; }
					if (!wx) { return; }
					wx.style.display = (wi.value === '') ? 'none' : 'block';
					});
					/* Light up the funnel icons on columns with active text. */
					var headRow2 = document.querySelector('.cs-specific-widget .cs-sortable thead tr');
					if (headRow2) {
					Array.prototype.slice.call(headRow2.children).forEach(function (th2, i3) {
					var ic = th2.querySelector('.cs-fico');
					if (!ic) { return; }
					ic.className = (colFilters[i3] === undefined) ? 'cs-fico' : 'cs-fico cs-fico-on';
					});
					}
					refreshViews();
					} catch (e) { }
					}
					/* Funnel icon on every visible column header: one click
					   turns filtering on (if needed) and drops the cursor in
					   that column's box. The icon lights up while its column
					   has an active filter. */
					function wireFilterIcons() {
					try {
					var headRow = document.querySelector('.cs-specific-widget .cs-sortable thead tr');
					if (!headRow) { return; }
					Array.prototype.slice.call(headRow.children).forEach(function (th, idx) {
					if (th.getAttribute('data-fico-wired') === '1') { return; }
					th.setAttribute('data-fico-wired', '1');
					var disp = '';
					try { disp = window.getComputedStyle(th).display; } catch (e2) { }
					if (disp === 'none') { return; }
					var ico = document.createElement('span');
					ico.className = 'cs-fico';
					ico.title = 'Filter this column';
					var svgNS = 'http://www.w3.org/2000/svg';
					var svg = document.createElementNS(svgNS, 'svg');
					svg.setAttribute('width', '11');
					svg.setAttribute('height', '11');
					svg.setAttribute('viewBox', '0 0 24 24');
					svg.setAttribute('fill', 'none');
					svg.setAttribute('stroke', 'currentColor');
					svg.setAttribute('stroke-width', '2');
					svg.setAttribute('stroke-linecap', 'round');
					svg.setAttribute('stroke-linejoin', 'round');
					var poly = document.createElementNS(svgNS, 'polygon');
					poly.setAttribute('points', '22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3');
					svg.appendChild(poly);
					ico.appendChild(svg);
					ico.onclick = function (ev) {
					/* Don't let the click also sort the column. */
					ev.stopPropagation();
					ev.preventDefault();
					if (!filterOn) { showFilters(); }
					var tr = ensureFilterRow();
					if (tr) {
					var td = tr.children[idx];
					if (td) {
					var inp = td.querySelector('.cs-colfilter');
					if (inp) { try { inp.focus(); } catch (e3) { } }
					}
					}
					};
					th.appendChild(ico);
					});
					} catch (e) { }
					}
					/* Quick-filter funnels inside the facet cells (Status,
					   Priority, Category, ...): click the funnel next to
					   "Payroll" and the grid narrows to Payroll. SVG only -
					   no text - so exports, search, and the card builders
					   that read cell text never see it. */
					function wireCellFilterIcons() {
					try {
					var labels = ['Status', 'Priority', 'Category', 'Case Type', 'Assigned To', 'Employee'];
					var svgNS = 'http://www.w3.org/2000/svg';
					dataRows().forEach(function (r) {
					if (r.getAttribute('data-cellfico-wired') === '1') { return; }
					r.setAttribute('data-cellfico-wired', '1');
					labels.forEach(function (label) {
					var idx = colIndex[label];
					if (idx === undefined) { return; }
					var cell = r.children[idx];
					if (!cell) { return; }
					var val = (cell.textContent || '').replace(/^\s+|\s+$/g, '');
					if (val === '') { return; }
					if (val === String.fromCharCode(8212)) { return; }
					var ico = document.createElement('span');
					ico.className = 'cs-cellfico';
					ico.title = 'Show only ' + label + ': ' + val;
					var svg = document.createElementNS(svgNS, 'svg');
					svg.setAttribute('width', '11');
					svg.setAttribute('height', '11');
					svg.setAttribute('viewBox', '0 0 24 24');
					svg.setAttribute('fill', 'none');
					svg.setAttribute('stroke', 'currentColor');
					svg.setAttribute('stroke-width', '2');
					svg.setAttribute('stroke-linecap', 'round');
					svg.setAttribute('stroke-linejoin', 'round');
					var poly = document.createElementNS(svgNS, 'polygon');
					poly.setAttribute('points', '22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3');
					svg.appendChild(poly);
					ico.appendChild(svg);
					ico.onclick = function (ev) {
					/* Not a row click: no expansion, no sort. */
					ev.stopPropagation();
					ev.preventDefault();
					if (!filterOn) { showFilters(); }
					var tr = ensureFilterRow();
					if (tr) {
					var td2 = tr.children[idx];
					if (td2) {
					var inp = td2.querySelector('.cs-colfilter');
					if (inp) { inp.value = val; }
					}
					}
					colFilters[idx] = val.toLowerCase();
					applyColFilters();
					};
					cell.appendChild(ico);
					});
					});
					} catch (e) { }
					}
					/* The funnels are the way IN to filtering: clicking any of
					   them reveals the filter row. The way OUT is the "Clear
					   filters" button, which only exists while filtering. */
					function showFilters() {
					try {
					filterOn = true;
					var tr = ensureFilterRow();
					if (tr) { tr.style.display = ''; }
					/* Filters live under the table's headers, so make sure
					   the table is the view being shown. */
					if (boardMode) {
					var fb = document.querySelector('.cs-specific-widget .cs-boardbtn');
					if (fb) { fb.click(); }
					} else if (cardsMode) {
					var fc = document.querySelector('.cs-specific-widget .cs-cardsbtn');
					if (fc) { fc.click(); }
					}
					setToolbarHeight();
					var cb = document.querySelector('.cs-specific-widget .cs-clearbtn');
					if (cb) { cb.style.display = ''; }
					} catch (e) { }
					}
					window.csClearFilters = function (btn) {
					try {
					filterOn = false;
					colFilters = {};
					Array.prototype.slice.call(document.querySelectorAll('.cs-specific-widget .cs-colfilter')).forEach(function (i2) {
					i2.value = '';
					});
					var tr = ensureFilterRow();
					if (tr) { tr.style.display = 'none'; }
					applyColFilters();
					var cb = document.querySelector('.cs-specific-widget .cs-clearbtn');
					if (cb) { cb.style.display = 'none'; }
					if (btn) { try { btn.blur(); } catch (e2) { } }
					} catch (e) { }
					};
					/* ============ Case-number hover preview ============
					   Hovering a Case # cell floats a small card with the
					   subject, status, and the full-text fields, so you can
					   peek without opening the case or expanding the row. */
					function ensureTip() {
					var root = document.querySelector('.cs-specific-widget');
					if (!root) { return null; }
					var tip = root.querySelector('.cs-tip');
					if (!tip) {
					tip = document.createElement('div');
					tip.className = 'cs-tip';
					root.appendChild(tip);
					}
					return tip;
					}
					function buildTipContent(tip, row) {
					while (tip.firstChild) { tip.removeChild(tip.firstChild); }
					var t = document.createElement('div');
					t.className = 'cs-tip-title';
					var dot = ' ' + String.fromCharCode(183) + ' ';
					t.textContent = 'Case ' + cellText(row, 'Case #') + dot + cellText(row, 'Status') + dot + cellText(row, 'Priority');
					tip.appendChild(t);
					var m = document.createElement('div');
					m.className = 'cs-tip-meta';
					var mtxt = cellText(row, 'Subject');
					var due = cellText(row, 'Due');
					if (due !== '') { mtxt = mtxt + dot + 'Due ' + due; }
					m.textContent = mtxt;
					tip.appendChild(m);
					var any = false;
					['Issue', 'Description', 'Resolution'].forEach(function (label) {
					var v = cellText(row, label);
					if (v === '') { return; }
					any = true;
					if (v.length > 220) { v = v.slice(0, 220) + String.fromCharCode(8230); }
					var l = document.createElement('div');
					l.className = 'cs-tip-lbl';
					l.textContent = label;
					var b = document.createElement('div');
					b.textContent = v;
					tip.appendChild(l);
					tip.appendChild(b);
					});
					if (!any) {
					var n = document.createElement('div');
					n.className = 'cs-tip-meta';
					n.textContent = 'No additional details on this case.';
					tip.appendChild(n);
					}
					}
					function wireCaseTooltips() {
					try {
					var w = document.querySelector('.cs-specific-widget');
					if (!w) { return; }
					if (w.getAttribute('data-tip-wired') === '1') { return; }
					w.setAttribute('data-tip-wired', '1');
					w.addEventListener('mouseover', function (ev) {
					try {
					var node = ev.target;
					var cell = null;
					while (node) {
					if (node.nodeType === 1) {
					if (/cs-casenum/.test(String(node.className || ''))) { cell = node; break; }
					if (node === w) { break; }
					}
					node = node.parentNode;
					}
					var tip = ensureTip();
					if (!tip) { return; }
					if (!cell) { tip.style.display = 'none'; return; }
					buildTipContent(tip, cell.parentNode);
					tip.style.display = 'block';
					var r = cell.getBoundingClientRect();
					tip.style.left = '0px';
					tip.style.top = '0px';
					var tw = tip.offsetWidth;
					var thh = tip.offsetHeight;
					var left = r.right + 10;
					if (left + tw + 8 > window.innerWidth) { left = Math.max(8, window.innerWidth - tw - 8); }
					var top = r.top;
					if (top + thh + 8 > window.innerHeight) { top = Math.max(8, window.innerHeight - thh - 8); }
					tip.style.left = left + 'px';
					tip.style.top = top + 'px';
					} catch (e2) { }
					});
					w.addEventListener('mouseleave', function () {
					try {
					var tip = ensureTip();
					if (tip) { tip.style.display = 'none'; }
					} catch (e2) { }
					});
					} catch (e) { }
					}
					/* Column resizing: a drag handle on the right edge of every
					   column header. Dragging sets an explicit width on the th
					   (min 48px), which the browser applies to the whole column.
					   stopPropagation keeps a resize drag from also firing the
					   shared script's click-to-sort on the same header. */
					function wireColumnResizers() {
					try {
					var heads = document.querySelectorAll('.cs-specific-widget th.sticky-header');
					Array.prototype.slice.call(heads).forEach(function (th) {
					if (th.getAttribute('data-resizer-wired') === '1') { return; }
					th.setAttribute('data-resizer-wired', '1');
					var grip = document.createElement('div');
					grip.className = 'cs-resizer';
					th.appendChild(grip);
					grip.addEventListener('click', function (ev) { ev.stopPropagation(); });
					grip.addEventListener('mousedown', function (ev) {
					ev.preventDefault();
					ev.stopPropagation();
					var startX = ev.pageX;
					var startW = th.offsetWidth;
					grip.className = 'cs-resizer cs-resizing';
					function onMove(mv) {
					var w = Math.max(48, startW + (mv.pageX - startX));
					th.style.width = w + 'px';
					th.style.minWidth = w + 'px';
					}
					function onUp() {
					grip.className = 'cs-resizer';
					document.removeEventListener('mousemove', onMove);
					document.removeEventListener('mouseup', onUp);
					savePrefs();
					}
					document.addEventListener('mousemove', onMove);
					document.addEventListener('mouseup', onUp);
					});
					});
					} catch (e) { }
					}
					/* The column-header cells pin at top: var(--cs-toolbar-h);
					   measure the toolbar row's real rendered height so the two
					   sticky layers meet exactly, whatever the font/zoom does. */
					function setToolbarHeight() {
					try {
					var root = document.querySelector('.cs-specific-widget');
					var bar = document.querySelector('.cs-specific-widget .cs-toolbar-band');
					if (root) {
					if (bar) {
					if (bar.offsetHeight > 0) {
					root.style.setProperty('--cs-toolbar-h', bar.offsetHeight + 'px');
					/* The column-filter row pins just under the column
					   headers, so its offset is band + header height. */
					var th0 = document.querySelector('.cs-specific-widget .cs-sortable thead th');
					if (th0) {
					if (th0.offsetHeight > 0) {
					root.style.setProperty('--cs-filter-top', (bar.offsetHeight + th0.offsetHeight) + 'px');
					}
					}
					}
					}
					}
					} catch (e) { }
					}
					/* Friendly header labels, belt-and-braces. The stylesheet
					   already maps known column aliases to display names, but
					   only on an exact case-sensitive match - if the proc's
					   alias casing drifts, the raw name leaks into the header.
					   This pass fixes any header that still shows a raw name:
					   known names (matched case-insensitively) get the official
					   label, anything else gets its CamelCase/underscores split
					   into words. Runs before the resizer grips are added, so
					   the header's only child is still its text node. */
					var HEADER_LABELS = {
					'casenumber': 'Case #', 'casestatus': 'Status', 'status': 'Status',
					'priority': 'Priority', 'category': 'Category', 'casetype': 'Case Type',
					'subject': 'Subject', 'assignedto': 'Assigned To', 'employee': 'Employee',
					'duedate': 'Due', 'createdate': 'Created', 'createtime': 'Created Time',
					'issue': 'Issue', 'description': 'Description', 'resolution': 'Resolution'
					};
					function prettyHeaders() {
					try {
					var heads = document.querySelectorAll('.cs-specific-widget th.sticky-header');
					Array.prototype.slice.call(heads).forEach(function (th) {
					var node = th.firstChild;
					if (!node) { return; }
					if (node.nodeType !== 3) { return; }
					var raw = (node.nodeValue || '').replace(/^\s+|\s+$/g, '');
					if (raw === '') { return; }
					var label = HEADER_LABELS[raw.toLowerCase()];
					if (!label) {
					/* Only rewrite single CamelCase/underscore tokens - a header
					   that already contains a space or '#' is already friendly. */
					if (/^[A-Za-z0-9_]+$/.test(raw)) {
					label = raw.replace(/_/g, ' ').replace(/([a-z0-9])([A-Z])/g, '$1 $2');
					}
					}
					if (label) { node.nodeValue = label; }
					});
					} catch (e) { }
					}
					function refit() { setToolbarHeight(); sizeTableWrap(); }
					function init() {
					/* A widget refresh can re-run this script over a DOM that
					   already carries highlights from the previous run - wipe
					   them so the page never starts with more than zero. */
					clearKbdHighlights();
					markOverdue();
					refit();
					wireRecordLinks();
					prettyHeaders();
					buildColIndex();
					wireSorting();
					wireColumnResizers();
					wireRowExpansion();
					wireCaseTooltips();
					wireFilterIcons();
					wireCellFilterIcons();
					wireKeyboardNav();
					restorePrefs();
					}
					if (document.readyState === 'loading') {
					document.addEventListener('DOMContentLoaded', init);
					} else {
					init();
					}
					window.addEventListener('resize', refit);
					window.addEventListener('load', refit);
					})();
				</script>
			</body>
			<script type="text/javascript" src="/Next/custom/oas_custom_expcsvsortsrch.js"></script>
		</html>
	</xsl:template>


	<!-- ================= named templates ================= -->

	<xsl:template name="chip">
		<xsl:param name="value"/>
		<xsl:param name="scale" select="'status'"/>
		<xsl:variable name="v" select="translate(normalize-space($value), $uc, $lc)"/>
		<xsl:choose>
			<xsl:when test="$v = ''">
				<span class="cs-muted">&#8212;</span>
			</xsl:when>
			<xsl:otherwise>
				<span>
					<xsl:attribute name="class">
						<xsl:text>cs-chip </xsl:text>
						<xsl:choose>
							<xsl:when test="$scale = 'priority'">
								<xsl:choose>
									<xsl:when test="contains($hotPriorities, concat('|', $v, '|'))">cs-chip-red</xsl:when>
									<xsl:when test="contains($midPriorities, concat('|', $v, '|'))">cs-chip-amber</xsl:when>
									<xsl:when test="contains($lowPriorities, concat('|', $v, '|'))">cs-chip-gray</xsl:when>
									<xsl:otherwise>cs-chip-purple</xsl:otherwise>
								</xsl:choose>
							</xsl:when>
							<xsl:when test="$scale = 'clientstatus'">
								<xsl:choose>
									<xsl:when test="contains($clientGreen, concat('|', $v, '|'))">cs-chip-green</xsl:when>
									<xsl:when test="contains($clientBlue,  concat('|', $v, '|'))">cs-chip-blue</xsl:when>
									<xsl:when test="contains($clientGray,  concat('|', $v, '|'))">cs-chip-gray</xsl:when>
									<xsl:otherwise>cs-chip-purple</xsl:otherwise>
								</xsl:choose>
							</xsl:when>
							<xsl:otherwise>
								<xsl:choose>
									<xsl:when test="contains($greenStatuses, concat('|', $v, '|'))">cs-chip-green</xsl:when>
									<xsl:when test="contains($amberStatuses, concat('|', $v, '|'))">cs-chip-amber</xsl:when>
									<xsl:when test="contains($blueStatuses,  concat('|', $v, '|'))">cs-chip-blue</xsl:when>
									<xsl:when test="contains($redStatuses,   concat('|', $v, '|'))">cs-chip-red</xsl:when>
									<xsl:otherwise>cs-chip-purple</xsl:otherwise>
								</xsl:choose>
							</xsl:otherwise>
						</xsl:choose>
					</xsl:attribute>
					<xsl:value-of select="normalize-space($value)"/>
				</span>
			</xsl:otherwise>
		</xsl:choose>
	</xsl:template>

	<!-- One label/value pair in the client context card. Renders nothing at
	     all when the value is blank, so the card never shows a stray label. -->
	<xsl:template name="cc-field">
		<xsl:param name="label"/>
		<xsl:param name="value"/>
		<xsl:param name="mono" select="0"/>
		<xsl:param name="date" select="0"/>
		<xsl:if test="normalize-space($value) != ''">
			<div class="cs-cc-field">
				<span class="cs-cc-lbl">
					<xsl:value-of select="$label"/>
				</span>
				<span>
					<xsl:attribute name="class">
						<xsl:text>cs-cc-val</xsl:text>
						<xsl:if test="$mono = 1">
							<xsl:text> cs-num</xsl:text>
						</xsl:if>
					</xsl:attribute>
					<xsl:choose>
						<xsl:when test="$date = 1">
							<xsl:call-template name="format-date">
								<xsl:with-param name="value" select="$value"/>
							</xsl:call-template>
						</xsl:when>
						<xsl:otherwise>
							<xsl:value-of select="normalize-space($value)"/>
						</xsl:otherwise>
					</xsl:choose>
				</span>
			</div>
		</xsl:if>
	</xsl:template>

	<xsl:template name="value-or-dash">
		<xsl:param name="value"/>
		<xsl:choose>
			<xsl:when test="normalize-space($value) = ''">
				<span class="cs-muted">&#8212;</span>
			</xsl:when>
			<xsl:otherwise>
				<xsl:value-of select="normalize-space($value)"/>
			</xsl:otherwise>
		</xsl:choose>
	</xsl:template>

	<!-- Dates render as MM/DD/YYYY. The SP may return ISO (YYYY-MM-DD, with
	     or without a trailing time part); anything not recognizably ISO
	     passes through unchanged rather than being mangled. -->
	<xsl:template name="format-date">
		<xsl:param name="value"/>
		<xsl:variable name="d" select="normalize-space($value)"/>
		<xsl:choose>
			<xsl:when test="string-length($d) &gt;= 10
							and substring($d, 5, 1) = '-' and substring($d, 8, 1) = '-'
							and string(number(substring($d, 1, 4))) != 'NaN'
							and string(number(substring($d, 6, 2))) != 'NaN'
							and string(number(substring($d, 9, 2))) != 'NaN'">
				<xsl:value-of select="concat(substring($d, 6, 2), '/', substring($d, 9, 2), '/', substring($d, 1, 4))"/>
			</xsl:when>
			<xsl:otherwise>
				<xsl:value-of select="$d"/>
			</xsl:otherwise>
		</xsl:choose>
	</xsl:template>

	<xsl:template name="date-or-dash">
		<xsl:choose>
			<xsl:when test="normalize-space(.) = ''">
				<span class="cs-muted">&#8212;</span>
			</xsl:when>
			<xsl:otherwise>
				<xsl:call-template name="format-date">
					<xsl:with-param name="value" select="."/>
				</xsl:call-template>
			</xsl:otherwise>
		</xsl:choose>
	</xsl:template>

	<xsl:template name="text-or-dash">
		<xsl:choose>
			<xsl:when test="normalize-space(.) = ''">
				<span class="cs-muted">&#8212;</span>
			</xsl:when>
			<xsl:otherwise>
				<xsl:value-of select="."/>
			</xsl:otherwise>
		</xsl:choose>
	</xsl:template>

	<xsl:template name="longtext">
		<xsl:choose>
			<xsl:when test="normalize-space(.) = ''">
				<span class="cs-muted">&#8212;</span>
			</xsl:when>
			<xsl:otherwise>
				<div class="cs-clamp">
					<xsl:attribute name="title">
						<xsl:value-of select="normalize-space(.)"/>
					</xsl:attribute>
					<xsl:value-of select="."/>
				</div>
			</xsl:otherwise>
		</xsl:choose>
	</xsl:template>


	<!-- ============ KPI strip ============
	     Called from both branches of the widget body: the data branch passes
	     the counts it computed over the rows, the no-data branch takes the
	     zero defaults so the tiles still render when the proc returned
	     nothing. Past Due has no parameter - it is always filled in by
	     markOverdue() from the rendered Due cells.
	     Icon colors are set via inline style rather than the
	     stroke attribute because var() only resolves in CSS,
	     not in SVG presentation attributes. -->
	<xsl:template name="kpi-strip">
		<xsl:param name="total" select="0"/>
		<xsl:param name="new"   select="0"/>
		<xsl:param name="prog"  select="0"/>
		<xsl:param name="hold"  select="0"/>
		<!-- Tiles double as filters: csKpiFilter narrows the grid to the
		     rows that tile counts; clicking the pressed tile (or Total)
		     clears it. New / In Progress / On Hold split the active
		     statuses; Past Due is date-based and filled by markOverdue(). -->
		<div class="cs-kpis">
			<div class="cs-kpi cs-kpi-total" onclick="csKpiFilter(this, 'total');" title="Show all cases">
				<div class="cs-kpi-top">
					<span class="cs-kpi-num">
						<xsl:value-of select="$total"/>
					</span>
					<svg class="cs-kpi-ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="stroke: var(--gray500);">
						<rect x="2" y="7" width="20" height="13" rx="2"/>
						<path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/>
					</svg>
				</div>
				<div class="cs-kpi-lbl">Total Cases</div>
			</div>
			<div class="cs-kpi cs-kpi-new" onclick="csKpiFilter(this, 'new');" title="Filter to new cases">
				<div class="cs-kpi-top">
					<span class="cs-kpi-num">
						<xsl:value-of select="$new"/>
					</span>
					<svg class="cs-kpi-ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="stroke: var(--blue500);">
						<circle cx="12" cy="12" r="9"/>
						<line x1="12" y1="8" x2="12" y2="16"/>
						<line x1="8" y1="12" x2="16" y2="12"/>
					</svg>
				</div>
				<div class="cs-kpi-lbl">New</div>
			</div>
			<div class="cs-kpi cs-kpi-prog" onclick="csKpiFilter(this, 'prog');" title="Filter to in-progress cases">
				<div class="cs-kpi-top">
					<span class="cs-kpi-num">
						<xsl:value-of select="$prog"/>
					</span>
					<svg class="cs-kpi-ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="stroke: var(--yellow600);">
						<circle cx="12" cy="12" r="9"/>
						<path d="M12 7v5l3 2"/>
					</svg>
				</div>
				<div class="cs-kpi-lbl">In Progress</div>
			</div>
			<div class="cs-kpi cs-kpi-hold" onclick="csKpiFilter(this, 'hold');" title="Filter to on-hold cases">
				<div class="cs-kpi-top">
					<span class="cs-kpi-num">
						<xsl:value-of select="$hold"/>
					</span>
					<svg class="cs-kpi-ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="stroke: var(--purple500);">
						<line x1="9" y1="6" x2="9" y2="18"/>
						<line x1="15" y1="6" x2="15" y2="18"/>
					</svg>
				</div>
				<div class="cs-kpi-lbl">On Hold</div>
			</div>
			<div class="cs-kpi cs-kpi-soon" onclick="csKpiFilter(this, 'soon');" title="Filter to cases due in the next 7 days">
				<div class="cs-kpi-top">
					<span class="cs-kpi-num csDueSoonCount">0</span>
					<svg class="cs-kpi-ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="stroke: var(--green600);">
						<rect x="3" y="5" width="18" height="17" rx="2"/>
						<line x1="16" y1="2" x2="16" y2="7"/>
						<line x1="8" y1="2" x2="8" y2="7"/>
						<line x1="3" y1="11" x2="21" y2="11"/>
					</svg>
				</div>
				<div class="cs-kpi-lbl">Due This Week</div>
			</div>
			<div class="cs-kpi cs-kpi-late" onclick="csKpiFilter(this, 'late');" title="Filter to past-due cases">
				<div class="cs-kpi-top">
					<span class="cs-kpi-num csOverdueCount">0</span>
					<svg class="cs-kpi-ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="stroke: var(--orange600);">
						<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/>
						<line x1="4" y1="22" x2="4" y2="15"/>
					</svg>
				</div>
				<div class="cs-kpi-lbl">Past Due</div>
			</div>
		</div>
	</xsl:template>

	<!-- Toolbar. The button/input/label nesting depth is
	     identical to the Book of Business report because
	     oas_custom_expcsvsortsrch.js walks up from `this`
	     to find the table. Restyled, not restructured. -->
	<!-- A standalone sticky band ABOVE the table (not inside its thead):
	     a sticky element cannot travel beyond its parent table, and in
	     board view the table is nearly empty, which let the toolbar
	     scroll away while lane headers stayed. As a direct child of the
	     scroll wrap it pins in both views. -->
	<xsl:template name="case-toolbar">
		<div class="cs-toolbar-band">
				<div class="cs-toolbar">
					<!-- The title attribute doubles as a version stamp: hover
					     "Cases" in the live widget to confirm which build is
					     actually deployed. Bump it when pasting a new version. -->
					<div class="label cs-toolbar-title" onDOMContentLoaded="setWidigtLabel(this);" title="build 2026-08-18.37">Cases</div>
					<div>
						<!-- data-label-off/on and data-title-off/on hold both faces
						     of this toggle: released it reads "My Cases", and once
						     the grid is filtered to the signed-in user csCaseMode
						     relabels it "All Cases" so the next click is
						     self-describing. -->
						<button class="bt cs-viewbtn" type="button" onclick="csCaseMode(this, 'open');" Title="Show only my open cases" data-label-off="My Cases" data-label-on="All Cases" data-title-off="Show only my open cases" data-title-on="Show all cases again">My Cases</button>
					</div>
					<div>
						<button class="bt cs-boardbtn" type="button" onclick="csToggleBoard(this);" Title="Switch between the table and a status board (B / T)"><u>B</u>oard</button>
						<button class="bt cs-cardsbtn" type="button" onclick="csToggleCards(this);" Title="Flip through cases one by one (C)"><u>C</u>ards</button>
						<button class="bt cs-clearbtn" type="button" onclick="csClearFilters(this);" style="display:none;" Title="Clear all column filters and hide the filter row (F)">Clear filters &#10005;</button>
					</div>
					<!-- Works in all views: groups the table's rows under
					     subheadings with counts, and regroups the board's
					     columns by the chosen field. "No grouping" restores the
					     flat list / the default status columns. -->
					<div class="cs-groupwrap">
						<select class="cs-groupsel" onchange="csGroupBy(this);" title="Group rows by a column">
							<option value="">Group by: None</option>
							<option value="category">Group by: Category</option>
							<option value="assignedto">Group by: Assigned To</option>
							<option value="status">Group by: Status</option>
							<option value="priority">Group by: Priority</option>
						</select>
					</div>
					<div>
						<!-- Search is self-owned (csFilterCases below) for the same
						     reason as the CSV export: the shared script's filter()
						     was unreliable here. It matches on every visible cell,
						     Case # included. -->
						<input type="text" class="searchBox" onkeyup="csFilterCases(this)" placeholder="Search cases"></input>
					</div>
					<div>
						<button class="bt cs-expandbtn" type="button" onclick="csExpandTab(this);" Title="Open this widget alone in a new browser tab (E)"><u>E</u>xpand &#8599;</button>
					</div>
					<div>
						<button class="bt" type="button" onclick="csExportCases(this);" Title="Export to CSV">Export to CSV</button>
					</div>
					<!-- Pinned to the toolbar's right edge. -->
					<div class="cs-toolbar-right">
						<button class="bt cs-darkbtn" type="button" onclick="csToggleDark(this);" Title="Switch to dark mode">&#9790;</button>
					</div>
				</div>
				<div style="clear: left;"></div>
		</div>
	</xsl:template>

</xsl:stylesheet>