{"id":19,"date":"2024-10-16T12:56:54","date_gmt":"2024-10-16T12:56:54","guid":{"rendered":"https:\/\/kvecsertac.theholler.org\/?page_id=19"},"modified":"2026-05-28T18:53:24","modified_gmt":"2026-05-28T18:53:24","slug":"home","status":"publish","type":"page","link":"https:\/\/kvecsertac.theholler.org\/","title":{"rendered":""},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"19\" class=\"elementor elementor-19\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-310e9c48 e-con-full e-flex e-con e-parent\" data-id=\"310e9c48\" data-element_type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-e87eaa2 elementor-widget elementor-widget-html\" data-id=\"e87eaa2\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n<meta charset=\"UTF-8\">\r\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\r\n<title>KVEC-SERTAC<\/title>\r\n<link rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\">\r\n<link rel=\"preconnect\" href=\"https:\/\/fonts.gstatic.com\" crossorigin>\r\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=Roboto:wght@400;500;600;700&family=Manrope:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\r\n<style>\r\n:root {\r\n  --ink:        #14222C;\r\n  --ink-soft:   #3D4F5C;\r\n  --ink-mute:   #556471;   \/* AA-compliant against #FFF (6.09:1), #E2EDDB (5.04:1), and #FAF7F1 (5.70:1) *\/\r\n  --paper:      #FAF7F1;\r\n  --paper-warm: #E2EDDB;   \/* soft sage \u2014 cadre strip, filter box, search input, row hover *\/\r\n  --paper-line: #D8E5CB;   \/* lighter sage \u2014 fine inner dividers (table rows, flyer meta) *\/\r\n  --sage-line:  #C9D9BC;   \/* sage line color inside green panels *\/\r\n  --sage-hover: #CFDFBE;   \/* hover background for list\/filter items on sage *\/\r\n  --blue:       #0F60AD;   \/* selected\/active state *\/\r\n  --card:       #FFFFFF;\r\n  --accent:     #1F5752;\r\n  --accent-2:   #B0763A;\r\n  --rule:       #C9D9BC;   \/* default border color \u2014 sage, matches the filter box *\/\r\n  --shadow-sm:  0 1px 2px rgba(20,34,44,.06), 0 2px 8px rgba(20,34,44,.04);\r\n  --shadow-md:  0 4px 14px rgba(20,34,44,.08), 0 12px 32px rgba(20,34,44,.06);\r\n  --display: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;\r\n  --sans:    'Manrope', system-ui, -apple-system, sans-serif;\r\n}\r\n\r\n* { box-sizing: border-box; margin: 0; padding: 0; }\r\n\r\nhtml, body { background: #FAF7F1; color: #14222C; }\r\nbody {\r\n  font-family: var(--sans);\r\n  font-size: 16px;\r\n  line-height: 1.55;\r\n  -webkit-font-smoothing: antialiased;\r\n  text-rendering: optimizeLegibility;\r\n}\r\n\r\na { color: inherit; text-decoration: none; }\r\n\r\n\/* Screen-reader-only utility: visible to AT, invisible on screen.\r\n * Used for form labels that have a visible placeholder but need a real label for a11y. *\/\r\n.visually-hidden {\r\n  position: absolute;\r\n  width: 1px;\r\n  height: 1px;\r\n  padding: 0;\r\n  margin: -1px;\r\n  overflow: hidden;\r\n  clip: rect(0, 0, 0, 0);\r\n  white-space: nowrap;\r\n  border: 0;\r\n}\r\n\r\n\/* Keyboard focus indicator \u2014 visible blue ring on all interactive controls\r\n * (508 \/ WCAG 2.4.7). Uses :focus-visible so mouse clicks don't trigger it. *\/\r\n:focus-visible {\r\n  outline: 2px solid #0F60AD;\r\n  outline-offset: 2px;\r\n  border-radius: 4px;\r\n}\r\n\r\n\/* ---------- Section: Flyers ---------- *\/\r\n.flyers-section {\r\n  padding: 56px 32px 56px;\r\n  max-width: 1400px;\r\n  margin: 0 auto;\r\n}\r\n.section-eyebrow {\r\n  font-family: var(--sans);\r\n  font-size: 12px;\r\n  font-weight: 600;\r\n  text-transform: uppercase;\r\n  letter-spacing: 0.16em;\r\n  color: #1F5752;\r\n  margin-bottom: 10px;\r\n}\r\n.section-title {\r\n  font-family: var(--display);\r\n  font-weight: 500;\r\n  font-size: clamp(28px, 3.4vw, 42px);\r\n  letter-spacing: -0.015em;\r\n  line-height: 1.1;\r\n  color: #14222C;\r\n  margin-bottom: 4px;\r\n}\r\n.section-title em {\r\n  font-style: normal;\r\n  color: #1F5752;\r\n  font-weight: 400;\r\n}\r\n.section-sub {\r\n  font-size: 14.5px;\r\n  color: #3D4F5C;\r\n  max-width: 60ch;\r\n  margin-bottom: 28px;\r\n}\r\n\r\n.carousel-wrap { position: relative; }\r\n.carousel-controls {\r\n  position: absolute;\r\n  top: -52px;\r\n  right: 0;\r\n  display: flex;\r\n  gap: 8px;\r\n}\r\n.carousel-btn {\r\n  width: 38px; height: 38px;\r\n  border-radius: 50%;\r\n  background: #FFFFFF;\r\n  border: 1px solid #C9D9BC;\r\n  color: #3D4F5C;\r\n  cursor: pointer;\r\n  display: flex;\r\n  align-items: center;\r\n  justify-content: center;\r\n  transition: all .18s ease;\r\n  font-family: var(--sans);\r\n  font-size: 16px;\r\n}\r\n.carousel-btn:hover { background: #0F60AD; color: #fff; border-color: #0F60AD; }\r\n.carousel-btn:disabled { opacity: 0.35; cursor: not-allowed; }\r\n.carousel-btn:disabled:hover { background: #FFFFFF; color: #3D4F5C; border-color: #C9D9BC; }\r\n\r\n.flyers-track {\r\n  display: flex;\r\n  gap: 20px;\r\n  overflow-x: auto;\r\n  scroll-snap-type: x mandatory;\r\n  scroll-behavior: smooth;\r\n  scrollbar-width: none;\r\n  padding-bottom: 8px;\r\n}\r\n.flyers-track::-webkit-scrollbar { display: none; }\r\n\r\n.flyer-card {\r\n  flex: 0 0 calc((100% - 40px) \/ 3);\r\n  scroll-snap-align: start;\r\n  background: #FFFFFF;\r\n  border-radius: 8px;\r\n  box-shadow: var(--shadow-sm);\r\n  overflow: hidden;\r\n  transition: transform .25s ease, box-shadow .25s ease;\r\n  display: flex;\r\n  flex-direction: column;\r\n}\r\n.flyer-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); }\r\n.flyer-img-link {\r\n  display: block;\r\n  background: #F0E8D6;\r\n  position: relative;\r\n  overflow: hidden;\r\n}\r\n.flyer-img-link img {\r\n  display: block;\r\n  width: 100%;\r\n  height: auto;\r\n  object-fit: contain;\r\n}\r\n.flyer-meta {\r\n  padding: 14px 16px 16px;\r\n  border-top: 1px solid #D8E5CB;\r\n}\r\n.flyer-date {\r\n  font-family: var(--display);\r\n  font-size: 13px;\r\n  font-weight: 600;\r\n  letter-spacing: 0.04em;\r\n  text-transform: uppercase;\r\n  color: #1F5752;\r\n  margin-bottom: 4px;\r\n}\r\n.flyer-title {\r\n  font-family: var(--display);\r\n  font-weight: 500;\r\n  font-size: 16px;\r\n  line-height: 1.3;\r\n  color: #14222C;\r\n}\r\n.flyer-title-empty { font-style: italic; color: #556471; font-weight: 400; }\r\n\r\n.state-msg { padding: 32px; text-align: center; color: #556471; font-size: 14px; }\r\n.state-msg.error { color: #8C2A1F; }\r\n\r\n\/* ---------- Cadre strip ---------- *\/\r\n.cadre-strip {\r\n  background: #E2EDDB;\r\n  border-top: 1px solid #C9D9BC;\r\n  border-bottom: 1px solid #C9D9BC;\r\n  padding: 36px 32px 40px;\r\n}\r\n.cadre-inner { max-width: 1400px; margin: 0 auto; }\r\n.cadre-header {\r\n  display: flex;\r\n  align-items: baseline;\r\n  justify-content: space-between;\r\n  margin-bottom: 22px;\r\n  gap: 16px;\r\n  flex-wrap: wrap;\r\n}\r\n.cadre-header h2 {\r\n  font-family: var(--display);\r\n  font-weight: 500;\r\n  font-size: 24px;\r\n  letter-spacing: -0.01em;\r\n  color: #14222C;\r\n}\r\n.cadre-header h2 em { font-style: normal; color: #1F5752; }\r\n.cadre-header p { font-size: 13.5px; color: #3D4F5C; }\r\n.cadre-grid {\r\n  display: grid;\r\n  grid-template-columns: repeat(4, 1fr);\r\n  gap: 18px;\r\n}\r\n.cadre-col {\r\n  background: #F0F5EA;\r\n  border: 1px solid #C9D9BC;\r\n  border-radius: 6px;\r\n  padding: 16px 16px 18px;\r\n  min-height: 150px;\r\n}\r\n.cadre-name {\r\n  font-family: var(--display);\r\n  font-weight: 600;\r\n  font-size: 15px;\r\n  color: #14222C;\r\n  margin-bottom: 2px;\r\n  line-height: 1.2;\r\n}\r\n.cadre-time {\r\n  font-family: var(--sans);\r\n  font-size: 12px;\r\n  font-weight: 500;\r\n  color: #3D4F5C;\r\n  letter-spacing: 0.02em;\r\n  margin-bottom: 10px;\r\n  padding-bottom: 8px;\r\n  border-bottom: 1px solid #C9D9BC;\r\n}\r\n.cadre-list { list-style: none; }\r\n.cadre-item {\r\n  padding: 6px 0;\r\n  font-size: 13.5px;\r\n  display: flex;\r\n  flex-direction: column;\r\n  border-bottom: 1px dashed rgba(167, 192, 147, .6);\r\n}\r\n.cadre-item:last-child { border-bottom: none; }\r\n.cadre-list--compact .cadre-item {\r\n  border-bottom: none;\r\n  padding: 3px 0;\r\n  break-inside: avoid;\r\n}\r\n\/* When a compact card has more than 5 dates, flow into 2 columns inside the box.\r\n * column-count + reasonable column-width = lets a single short list stay one column. *\/\r\n.cadre-list--compact:has(> li:nth-child(6)) {\r\n  column-count: 2;\r\n  column-gap: 14px;\r\n}\r\n.cadre-item a { color: #14222C; transition: color .18s; }\r\n.cadre-item a:hover { color: #1F5752; }\r\n.cadre-date-line {\r\n  font-family: var(--display);\r\n  font-weight: 600;\r\n  font-size: 13px;\r\n  color: #1F5752;\r\n  letter-spacing: 0.02em;\r\n}\r\n.cadre-item-title { color: #3D4F5C; font-size: 13px; margin-top: 1px; }\r\n.cadre-empty { font-size: 13px; color: #556471; font-style: italic; padding: 4px 0; }\r\n\r\n\/* ---------- Resource hub ---------- *\/\r\n.resource-hub {\r\n  max-width: 1400px;\r\n  margin: 0 auto;\r\n  padding: 56px 32px 80px;\r\n}\r\n.resource-buttons {\r\n  display: grid;\r\n  grid-template-columns: repeat(6, minmax(0, 1fr));\r\n  gap: 10px;\r\n  margin-bottom: 36px;\r\n}\r\n.resource-btn {\r\n  font-family: var(--sans);\r\n  font-size: 13px;\r\n  font-weight: 600;\r\n  letter-spacing: 0.02em;\r\n  background: #FFFFFF;\r\n  border: 1px solid #C9D9BC;\r\n  color: #3D4F5C;\r\n  padding: 16px 12px;\r\n  border-radius: 6px;\r\n  cursor: pointer;\r\n  text-align: center;\r\n  line-height: 1.25;\r\n  white-space: normal;\r\n  overflow-wrap: break-word;\r\n  transition: all .18s ease;\r\n  position: relative;\r\n}\r\n.resource-btn:hover {\r\n  background: #E2EDDB;\r\n  border-color: #C9D9BC;\r\n  color: #14222C;\r\n  transform: translateY(-1px);\r\n  box-shadow: var(--shadow-sm);\r\n}\r\n.resource-btn.active {\r\n  background: #0F60AD;\r\n  color: #FAF7F1;\r\n  border-color: #0F60AD;\r\n}\r\n.resource-btn.active::after {\r\n  content: '';\r\n  position: absolute;\r\n  bottom: -10px;\r\n  left: 50%;\r\n  transform: translateX(-50%) rotate(45deg);\r\n  width: 10px;\r\n  height: 10px;\r\n  background: #0F60AD;\r\n}\r\n\r\n.resource-panel-wrap {\r\n  background: #FFFFFF;\r\n  border: 1px solid #C9D9BC;\r\n  border-radius: 8px;\r\n  min-height: 320px;\r\n  padding: 36px;\r\n}\r\n.resource-panel { display: none; }\r\n.resource-panel.active { display: block; animation: fadeIn .25s ease; }\r\n@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }\r\n\r\n.panel-header {\r\n  display: flex;\r\n  align-items: flex-start;\r\n  justify-content: space-between;\r\n  gap: 24px;\r\n  padding-bottom: 18px;\r\n  border-bottom: 1px solid #C9D9BC;\r\n  margin-bottom: 22px;\r\n  flex-wrap: wrap;\r\n}\r\n.panel-title {\r\n  font-family: var(--display);\r\n  font-weight: 500;\r\n  font-size: 26px;\r\n  line-height: 1.2;\r\n  letter-spacing: -0.01em;\r\n  color: #14222C;\r\n}\r\n.panel-title em { font-style: normal; color: #1F5752; font-weight: 400; }\r\n.panel-count {\r\n  font-size: 13px;\r\n  color: #556471;\r\n  font-family: var(--sans);\r\n  margin-top: 4px;\r\n}\r\n\r\n.panel-actions {\r\n  display: flex;\r\n  flex-direction: column;\r\n  gap: 8px;\r\n}\r\n.panel-action-btn {\r\n  font-family: var(--sans);\r\n  font-size: 13.5px;\r\n  font-weight: 600;\r\n  background: #0F60AD;\r\n  border: 1px solid #0F60AD;\r\n  color: #fff;\r\n  padding: 10px 14px;\r\n  border-radius: 6px;\r\n  cursor: pointer;\r\n  text-decoration: none;\r\n  transition: filter .14s, transform .14s;\r\n  display: flex;\r\n  align-items: center;\r\n  justify-content: center;\r\n  text-align: center;\r\n  width: 100%;\r\n}\r\n.panel-action-btn:hover {\r\n  filter: brightness(0.92);\r\n  transform: translateY(-1px);\r\n}\r\n\r\n.panel-search-bar {\r\n  display: flex;\r\n  align-items: center;\r\n  gap: 8px;\r\n  margin-bottom: 16px;\r\n}\r\n.panel-search-bar input {\r\n  font-family: var(--sans);\r\n  font-size: 13.5px;\r\n  padding: 8px 12px;\r\n  border: 1px solid #C9D9BC;\r\n  border-radius: 5px;\r\n  background: #E2EDDB;\r\n  color: #14222C;\r\n  flex: 1;\r\n  max-width: 340px;\r\n}\r\n.panel-search-bar input:focus { border-color: #0F60AD; }\r\n\r\n.panel-grid {\r\n  display: grid;\r\n  grid-template-columns: 1fr 240px;\r\n  gap: 32px;\r\n  align-items: start;\r\n}\r\n@media (max-width: 900px) { .panel-grid { grid-template-columns: 1fr; } }\r\n\r\n.resource-table {\r\n  width: 100%;\r\n  border-collapse: collapse;\r\n  font-size: 16px;\r\n  background: #FFFFFF;\r\n}\r\n.resource-table thead th {\r\n  text-align: left;\r\n  font-size: 13px;\r\n  font-weight: 600;\r\n  letter-spacing: 0.1em;\r\n  text-transform: uppercase;\r\n  color: #556471;\r\n  background: #FFFFFF;\r\n  padding: 8px 10px;\r\n  border-bottom: 2px solid #C9D9BC;\r\n}\r\n.resource-table tbody td {\r\n  padding: 12px 10px;\r\n  border-bottom: 1px solid #D8E5CB;\r\n  vertical-align: top;\r\n  background: #FFFFFF;\r\n}\r\n.resource-table tbody tr:hover td { background: #E2EDDB; }\r\n.resource-table a { color: #0F60AD; text-decoration: none; }\r\n.resource-table a:hover { color: #1F5752; }\r\n.resource-table .type-cell { color: #3D4F5C; white-space: nowrap; }\r\n\r\n\/* ---------- Side filter list ---------- *\/\r\n.type-filter {\r\n  background: #E2EDDB;\r\n  border: 1px solid #C9D9BC;\r\n  border-radius: 6px;\r\n  padding: 16px 18px 18px;\r\n}\r\n.type-filter h3 {\r\n  font-family: var(--display);\r\n  font-weight: 600;\r\n  font-size: 13px;\r\n  letter-spacing: 0.06em;\r\n  text-transform: uppercase;\r\n  color: #3D4F5C;\r\n  margin-bottom: 12px;\r\n}\r\n.type-filter ul { list-style: none; }\r\n.type-filter li { margin-bottom: 2px; }\r\n.type-filter button {\r\n  font-family: var(--sans);\r\n  font-size: 13.5px;\r\n  background: transparent;\r\n  border: none;\r\n  color: #3D4F5C;\r\n  text-align: left;\r\n  cursor: pointer;\r\n  padding: 5px 8px;\r\n  border-radius: 4px;\r\n  width: 100%;\r\n  transition: background .14s, color .14s;\r\n  display: flex;\r\n  justify-content: space-between;\r\n  align-items: center;\r\n  gap: 8px;\r\n}\r\n.type-filter button:hover { background: #CFDFBE; color: #14222C; }\r\n.type-filter button.active {\r\n  background: #0F60AD;\r\n  color: #fff;\r\n  font-weight: 600;\r\n}\r\n.type-filter button.active .count { color: #fff; opacity: .8; }\r\n.type-filter .count {\r\n  font-size: 12px;\r\n  color: #556471;\r\n  font-variant-numeric: tabular-nums;\r\n}\r\n.type-filter .empty { font-style: italic; color: #556471; font-size: 13px; }\r\n\r\n.sidebar {\r\n  display: flex;\r\n  flex-direction: column;\r\n  gap: 12px;\r\n  position: sticky;\r\n  top: 16px;\r\n}\r\n\r\n.pagination {\r\n  display: flex;\r\n  align-items: center;\r\n  justify-content: center;\r\n  gap: 12px;\r\n  margin-top: 18px;\r\n  padding-top: 18px;\r\n  border-top: 1px solid #D8E5CB;\r\n  font-size: 13.5px;\r\n  color: #3D4F5C;\r\n}\r\n.page-btn {\r\n  background: #FFFFFF;\r\n  border: 1px solid #C9D9BC;\r\n  width: 30px;\r\n  height: 30px;\r\n  border-radius: 5px;\r\n  cursor: pointer;\r\n  font-size: 15px;\r\n  color: #3D4F5C;\r\n  display: flex;\r\n  align-items: center;\r\n  justify-content: center;\r\n  transition: all .14s;\r\n  flex-shrink: 0;\r\n}\r\n.page-btn:hover:not(:disabled) {\r\n  background: #0F60AD;\r\n  color: #fff;\r\n  border-color: #0F60AD;\r\n}\r\n.page-btn:disabled { opacity: 0.3; cursor: not-allowed; }\r\n.page-status {\r\n  font-family: var(--display);\r\n  font-weight: 600;\r\n  font-variant-numeric: tabular-nums;\r\n  white-space: nowrap;\r\n}\r\n\r\n\/* ---------- Responsive ---------- *\/\r\n@media (max-width: 1200px) {\r\n  .resource-buttons { grid-template-columns: repeat(3, minmax(0, 1fr)); }\r\n  .cadre-grid { grid-template-columns: repeat(2, 1fr); }\r\n}\r\n@media (max-width: 720px) {\r\n  .flyers-section, .resource-hub { padding-left: 18px; padding-right: 18px; padding-top: 32px; }\r\n  .cadre-strip { padding-left: 18px; padding-right: 18px; }\r\n  .flyer-card { flex: 0 0 calc((100% - 20px) \/ 2); }\r\n  .resource-buttons { grid-template-columns: repeat(2, minmax(0, 1fr)); }\r\n  .resource-btn { padding: 14px 8px; }\r\n  .cadre-grid { grid-template-columns: 1fr; }\r\n  .carousel-controls { position: static; margin-top: 14px; justify-content: flex-end; }\r\n  .resource-panel-wrap { padding: 22px 18px; }\r\n}\r\n<\/style>\r\n<\/head>\r\n<body>\r\n\r\n<main>\r\n\r\n  <!-- ===== Upcoming Conference Flyers ===== -->\r\n  <section class=\"flyers-section\" id=\"flyers\">\r\n    <div class=\"section-eyebrow\">Upcoming<\/div>\r\n    <h1 class=\"section-title\">Conferences &amp; <em>Trainings<\/em><\/h1>\r\n    <p class=\"section-sub\">Click any flyer to register. Scroll for more upcoming events.<\/p>\r\n\r\n    <div class=\"carousel-wrap\">\r\n      <div class=\"carousel-controls\">\r\n        <button class=\"carousel-btn\" id=\"flyer-prev\" aria-label=\"Previous flyers\">\u2039<\/button>\r\n        <button class=\"carousel-btn\" id=\"flyer-next\" aria-label=\"Next flyers\">\u203a<\/button>\r\n      <\/div>\r\n      <div class=\"flyers-track\" id=\"flyers-track\">\r\n        <div class=\"state-msg\">Loading upcoming conferences\u2026<\/div>\r\n      <\/div>\r\n    <\/div>\r\n  <\/section>\r\n\r\n  <!-- ===== Cadre Strip ===== -->\r\n  <section class=\"cadre-strip\" id=\"cadre\">\r\n    <div class=\"cadre-inner\">\r\n      <div class=\"cadre-header\">\r\n        <h2>Cadre &amp; <em>Network Schedules<\/em><\/h2>\r\n      <\/div>\r\n      <div class=\"cadre-grid\" id=\"cadre-grid\">\r\n        <div class=\"state-msg\">Loading cadre schedules\u2026<\/div>\r\n      <\/div>\r\n    <\/div>\r\n  <\/section>\r\n\r\n  <!-- ===== Resource Hub ===== -->\r\n  <section class=\"resource-hub\" id=\"resources\">\r\n    <div class=\"section-eyebrow\">Library<\/div>\r\n    <h2 class=\"section-title\">Resource <em>Lists<\/em><\/h2>\r\n    <p class=\"section-sub\">Choose a category to browse resources, filter the table by type, or search.<\/p>\r\n\r\n    <div class=\"resource-buttons\" id=\"resource-buttons\"><\/div>\r\n    <div class=\"resource-panel-wrap\" id=\"resource-panels\"><\/div>\r\n  <\/section>\r\n\r\n<\/main>\r\n\r\n<script>\r\n\/* ============================================================\r\n *  KVEC-SERTAC Dashboard\r\n *  Pulls from a published Google Sheet\r\n *    - \"Linked Dates\"      \u2192 first tab  (flyers + cadres)\r\n *    - \"Form Responses 1\"  \u2192 second tab (resource lists)\r\n *  ============================================================ *\/\r\n\r\n\/* ---------- CONFIG ---------- *\/\r\n\r\nconst LINKED_DATES_CSV =\r\n  'https:\/\/docs.google.com\/spreadsheets\/d\/e\/2PACX-1vQQjMu8a04bwlVkXBk3bXI5NfB6G7c6FhHhTwwrrd7vjJW2yDLZ7D39xGcCJSccIj-ETr8yfl_dDcp3\/pub?output=csv';\r\n\r\n\/* Published-to-web CSV URL for the \"Form Responses 1\" tab. *\/\r\nconst FORM_RESPONSES_CSV =\r\n  'https:\/\/docs.google.com\/spreadsheets\/d\/e\/2PACX-1vQQjMu8a04bwlVkXBk3bXI5NfB6G7c6FhHhTwwrrd7vjJW2yDLZ7D39xGcCJSccIj-ETr8yfl_dDcp3\/pub?gid=1783453054&single=true&output=csv';\r\n\r\n\/* Cadre groups for the soft-green strip.\r\n *   match  = exact value found in the sheet's Type column\r\n *   label  = display name in the dashboard\r\n *   time   = standing meeting time shown beneath the label\r\n * Order matters: first 4 form row 1, next 4 form row 2. *\/\r\nconst CADRES = [\r\n  { match: 'New Teacher Cadre',                 label: 'New Teacher Cadre',                       time: '3:30 \u2013 4:30 pm' },\r\n  { match: 'Autism Hour Cadre',                 label: 'Autism Hour Cadre',                       time: '3:30 \u2013 4:30 pm' },\r\n  { match: 'Behavior Cadre',                    label: 'Behavior Cadre',                          time: '3:30 \u2013 4:30 pm' },\r\n  { match: 'Low Incidence Cadre',               label: 'Low Incidence Cadre',                     time: '12:30 \u2013 1:30 pm' },\r\n  { match: 'Principal Support Network Meeting', label: 'Principal Support Network Meetings',     time: '9 \u2013 10:30 am' },\r\n  { match: 'Autism Problem-Solving Team',       label: 'Autism Problem-Solving Team',            time: '1 \u2013 2:30 pm',    hideTitle: true },\r\n  { match: 'DoSE Meeting',                      label: 'Regional Director of Special Education', time: '9 am \u2013 2 pm',    hideTitle: true },\r\n  { match: 'UFLI Connect',                      label: 'UFLI Connect',                           time: '3:30 \u2013 4:30 pm', hideTitle: true }\r\n];\r\n\r\n\/* Form Responses 1 schema, verified against the published sheet:\r\n *\r\n *   A: Timestamp\r\n *   B: Training Name             (table column 1)\r\n *   C: Date\r\n *   D: Type                      (main category: Resource, Cadre, Transition, \u2026)\r\n *   E: Presenter(s)\r\n *   F: Duration (hrs)\r\n *   G: Resource Link             (href for column 1)\r\n *   H: Resource Type             (e.g. Administrator, Autism, Behavior, \u2026)\r\n *   I: Paraeducator Resource Type\r\n *   J: Transition Resource Type\r\n *   K: Additional Details\r\n *   L: Cadre Resource Type\r\n *\/\r\nconst COL_NAME = 'B';   \/\/ Training Name\r\nconst COL_LINK = 'G';   \/\/ Resource Link\r\n\r\n\/* The 6 Resource Hub buttons.\r\n *\r\n *   label:       text shown on the tab button itself\r\n *   panelTitle:  longer heading shown above the table when the list is open\r\n *                (falls back to `label` if not set)\r\n *   filter:      predicate run against each Form Responses 1 row\r\n *   typeCol:     column letter (or array of letters) whose value shows in the\r\n *                table's Type cell. If an array, the first non-blank value wins\r\n *                \u2014 this is needed for Paraeducator, whose two filter branches\r\n *                populate different columns (I vs H).\r\n *   actions:     array of up to 2 static link buttons. Each: { label, url }.\r\n *                Leave as [] to show nothing.\r\n *\/\r\nconst RESOURCE_BUTTONS = [\r\n  {\r\n    id: 'teacher',\r\n    label: 'Teacher',\r\n    panelTitle: 'Teacher Resources for Professional Learning - No Certificates Available',\r\n    filter: r => col(r,'D') === 'Resource' && col(r,'H') !== 'Administrator',\r\n    typeCol: 'H',\r\n    actions: []\r\n  },\r\n  {\r\n    id: 'administrator',\r\n    label: 'Administrator',\r\n    panelTitle: 'Administrator Resources',\r\n    filter: r => col(r,'H') === 'Administrator',\r\n    typeCol: 'D',\r\n    actions: [\r\n      { label: 'KY DoSE Site', url: 'https:\/\/www.kydose.org\/' }\r\n    ]\r\n  },\r\n  {\r\n    id: 'transition',\r\n    label: 'Transition',\r\n    panelTitle: 'Transition Resources',\r\n    filter: r => col(r,'D') === 'Transition',\r\n    typeCol: 'J',\r\n    actions: [\r\n      { label: 'KVEC Transition', url: 'https:\/\/www.theholler.org\/kvec-transition\/' },\r\n      { label: 'ARP Transition',  url: 'https:\/\/www.theholler.org\/arp\/' }\r\n    ]\r\n  },\r\n  {\r\n    id: 'professional-development',\r\n    label: 'Professional Development',\r\n    panelTitle: 'Professional Development Opportunities',\r\n    filter: r => {\r\n      const t = col(r,'D');\r\n      return t !== '' && !['Paraeducator','Resource','Transition','Cadre'].includes(t);\r\n    },\r\n    typeCol: 'D',\r\n    actions: []\r\n  },\r\n  {\r\n    id: 'paraeducator',\r\n    label: 'Paraeducator Trainings',\r\n    panelTitle: 'Paraeducator Professional Development Opportunities',\r\n    \/\/ (Note: actual data uses \"Mathematics\", not \"Math\")\r\n    filter: r => {\r\n      const type = col(r,'D');\r\n      const rt   = col(r,'H');\r\n      return type === 'Paraeducator' || ['Autism','Behavior','Literacy','Mathematics'].includes(rt);\r\n    },\r\n    typeCol: ['I','H'],\r\n    actions: [\r\n      { label: 'CIBRIS Trainings', url: 'https:\/\/training.cibrs.com\/' }\r\n    ]\r\n  },\r\n  {\r\n    id: 'cadres',\r\n    label: 'Cadres',\r\n    panelTitle: 'Cadre Resources',\r\n    filter: r => col(r,'L') !== '',\r\n    typeCol: 'L',\r\n    actions: []\r\n  }\r\n];\r\n\r\n\/* ---------- Utilities ---------- *\/\r\n\r\nfunction parseCSV(text) {\r\n  const rows = [];\r\n  let row = [], field = '', inQuotes = false;\r\n  for (let i = 0; i < text.length; i++) {\r\n    const c = text[i];\r\n    if (inQuotes) {\r\n      if (c === '\"') {\r\n        if (text[i + 1] === '\"') { field += '\"'; i++; }\r\n        else inQuotes = false;\r\n      } else field += c;\r\n    } else {\r\n      if (c === '\"') inQuotes = true;\r\n      else if (c === ',') { row.push(field); field = ''; }\r\n      else if (c === '\\n' || c === '\\r') {\r\n        if (c === '\\r' && text[i + 1] === '\\n') i++;\r\n        row.push(field); rows.push(row);\r\n        row = []; field = '';\r\n      } else field += c;\r\n    }\r\n  }\r\n  if (field !== '' || row.length) { row.push(field); rows.push(row); }\r\n  while (rows.length && rows[rows.length - 1].every(v => v === '')) rows.pop();\r\n  return rows;\r\n}\r\n\r\n\/* Convert CSV rows-of-arrays \u2192 array of objects keyed by header AND keep\r\n * a parallel array indexed by header position so we can look up by column\r\n * letter. Returns { rows, headers }. *\/\r\nfunction csvToTable(rows) {\r\n  if (!rows.length) return { rows: [], headers: [] };\r\n  const headers = rows[0].map(h => h.trim());\r\n  const objs = rows.slice(1).map(r => {\r\n    const obj = { __cells: r };           \/\/ keep raw cells for letter lookup\r\n    headers.forEach((h, i) => { obj[h] = (r[i] ?? '').trim(); });\r\n    return obj;\r\n  });\r\n  return { rows: objs, headers };\r\n}\r\n\r\n\/* Lookup a cell's value by column letter (A, B, C, \u2026, Z, AA, AB, \u2026). *\/\r\nfunction col(row, letter) {\r\n  let idx = 0;\r\n  for (let i = 0; i < letter.length; i++) {\r\n    idx = idx * 26 + (letter.toUpperCase().charCodeAt(i) - 64);\r\n  }\r\n  idx -= 1;\r\n  const v = (row.__cells && row.__cells[idx] != null) ? String(row.__cells[idx]) : '';\r\n  return v.trim();\r\n}\r\n\r\n\/* Resolve a button's typeCol (string or array of letters) to a display value.\r\n * For an array, return the first letter whose cell has a non-blank value. *\/\r\nfunction typeColValue(row, typeCol) {\r\n  if (Array.isArray(typeCol)) {\r\n    for (const letter of typeCol) {\r\n      const v = col(row, letter);\r\n      if (v) return v;\r\n    }\r\n    return '';\r\n  }\r\n  return col(row, typeCol);\r\n}\r\n\r\nfunction parseSheetDate(str) {\r\n  if (!str) return null;\r\n  const m = str.match(\/^(\\d{1,2})\\\/(\\d{1,2})\\\/(\\d{4})\/);\r\n  if (!m) return null;\r\n  const d = new Date(+m[3], +m[1] - 1, +m[2]);\r\n  return isNaN(d) ? null : d;\r\n}\r\n\r\n\/* Parse a full \"M\/D\/YYYY H:M:S\" timestamp into ms since epoch. *\/\r\nfunction parseTimestamp(str) {\r\n  if (!str) return 0;\r\n  const m = String(str).match(\/^(\\d{1,2})\\\/(\\d{1,2})\\\/(\\d{4})(?:\\s+(\\d{1,2}):(\\d{2})(?::(\\d{2}))?)?\/);\r\n  if (!m) return 0;\r\n  return new Date(+m[3], +m[1] - 1, +m[2], +(m[4] || 0), +(m[5] || 0), +(m[6] || 0)).getTime();\r\n}\r\n\r\n\/* Per-button display tweaks for the Type cell + side-filter labels.\r\n * Cadre values like \"New and Emerging Teacher Cadre\" become\r\n * \"New and Emerging Teacher\" so they fit the narrow filter rail. *\/\r\nfunction formatTypeLabel(value, buttonId) {\r\n  if (buttonId === 'cadres' && value.endsWith(' Cadre')) {\r\n    return value.slice(0, -' Cadre'.length);\r\n  }\r\n  return value;\r\n}\r\n\r\nfunction formatDate(d) {\r\n  if (!d) return '';\r\n  return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });\r\n}\r\n\r\nfunction todayMidnight() {\r\n  const t = new Date();\r\n  return new Date(t.getFullYear(), t.getMonth(), t.getDate());\r\n}\r\n\r\nfunction normalizeImageUrl(url) {\r\n  if (!url) return '';\r\n  url = url.trim();\r\n  const drive = url.match(\/drive\\.google\\.com\\\/(?:file\\\/d\\\/|open\\?id=|uc\\?(?:export=[^&]+&)?id=)([A-Za-z0-9_-]{10,})\/);\r\n  if (drive) return 'https:\/\/lh3.googleusercontent.com\/d\/' + drive[1];\r\n  const docs = url.match(\/docs\\.google\\.com\\\/uc\\?(?:export=[^&]+&)?id=([A-Za-z0-9_-]{10,})\/);\r\n  if (docs) return 'https:\/\/lh3.googleusercontent.com\/d\/' + docs[1];\r\n  if (url.startsWith('http:\/\/kvecsertac.theholler.org')) {\r\n    url = 'https:\/\/' + url.slice('http:\/\/'.length);\r\n  }\r\n  return url;\r\n}\r\n\r\nfunction normalizeRegUrl(url) {\r\n  if (!url) return '';\r\n  return url.trim().split(\/\\s+\/)[0].replace(\/[.,;:]+$\/, '');\r\n}\r\n\r\nfunction esc(s) {\r\n  return String(s ?? '').replace(\/[&<>\"']\/g, c => (\r\n    { '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c]\r\n  ));\r\n}\r\n\r\n\/* ---------- Renderers: Flyers ---------- *\/\r\n\r\nfunction renderFlyers(rows) {\r\n  const track = document.getElementById('flyers-track');\r\n  const today = todayMidnight();\r\n\r\n  const upcoming = rows\r\n    .filter(r => r.Type === 'Conference (Flyer) - JPEG Link')\r\n    .map(r => ({ ...r, _date: parseSheetDate(r.Date) }))\r\n    .filter(r => r._date && r._date >= today && r['Image Link'])\r\n    .sort((a, b) => a._date - b._date);\r\n\r\n  if (!upcoming.length) {\r\n    track.innerHTML = '<div class=\"state-msg\">No upcoming conferences scheduled \u2014 check back soon.<\/div>';\r\n    return;\r\n  }\r\n\r\n  track.innerHTML = upcoming.map(r => {\r\n    const img   = normalizeImageUrl(r['Image Link']);\r\n    const reg   = normalizeRegUrl(r['Registration Link']);\r\n    const title = r.Title || '';\r\n    const titleHtml = title ? esc(title) : '<span class=\"flyer-title-empty\">Conference flyer<\/span>';\r\n    const altText = title || 'Conference flyer for ' + r.Date;\r\n    const href = reg || img;\r\n    const targetAttr = href ? ' target=\"_blank\" rel=\"noopener noreferrer\"' : '';\r\n\r\n    return `\r\n      <article class=\"flyer-card\">\r\n        <a class=\"flyer-img-link\" href=\"${esc(href)}\"${targetAttr}>\r\n          <img decoding=\"async\" loading=\"lazy\" src=\"${esc(img)}\" alt=\"${esc(altText)}\"\r\n               onerror=\"this.style.background='#E2EDDB';this.alt='Image could not be loaded';\">\r\n        <\/a>\r\n        <div class=\"flyer-meta\">\r\n          <div class=\"flyer-date\">${esc(formatDate(r._date))}<\/div>\r\n          <div class=\"flyer-title\">${titleHtml}<\/div>\r\n        <\/div>\r\n      <\/article>\r\n    `;\r\n  }).join('');\r\n\r\n  const prev = document.getElementById('flyer-prev');\r\n  const next = document.getElementById('flyer-next');\r\n  const card = track.querySelector('.flyer-card');\r\n  const step = () => (card ? card.getBoundingClientRect().width + 20 : 300);\r\n  prev.onclick = () => track.scrollBy({ left: -step(), behavior: 'smooth' });\r\n  next.onclick = () => track.scrollBy({ left:  step(), behavior: 'smooth' });\r\n  const updateBtns = () => {\r\n    prev.disabled = track.scrollLeft <= 2;\r\n    next.disabled = track.scrollLeft + track.clientWidth >= track.scrollWidth - 2;\r\n  };\r\n  track.addEventListener('scroll', updateBtns, { passive: true });\r\n  requestAnimationFrame(updateBtns);\r\n}\r\n\r\n\/* ---------- Renderers: Cadres ---------- *\/\r\n\r\nfunction renderCadres(rows) {\r\n  const grid = document.getElementById('cadre-grid');\r\n  const today = todayMidnight();\r\n\r\n  grid.innerHTML = CADRES.map(({ match, label, time, hideTitle }) => {\r\n    const upcoming = rows\r\n      .filter(r => r.Type === match)\r\n      .map(r => ({ ...r, _date: parseSheetDate(r.Date) }))\r\n      .filter(r => r._date && r._date >= today)\r\n      .sort((a, b) => a._date - b._date);\r\n\r\n    const items = upcoming.length\r\n      ? `<ul class=\"cadre-list${hideTitle ? ' cadre-list--compact' : ''}\">${upcoming.map(r => {\r\n          const reg = normalizeRegUrl(r['Registration Link']);\r\n          const dateStr = formatDate(r._date);\r\n          const title = !hideTitle && r.Title ? r.Title : '';\r\n          const inner = `\r\n            <span class=\"cadre-date-line\">${esc(dateStr)}<\/span>\r\n            ${title ? `<span class=\"cadre-item-title\">${esc(title)}<\/span>` : ''}`;\r\n          return `<li class=\"cadre-item\">${\r\n            reg\r\n              ? `<a href=\"${esc(reg)}\" target=\"_blank\" rel=\"noopener noreferrer\">${inner}<\/a>`\r\n              : inner\r\n          }<\/li>`;\r\n        }).join('')}<\/ul>`\r\n      : '<div class=\"cadre-empty\">No upcoming meetings \u2014 check back soon.<\/div>';\r\n\r\n    return `\r\n      <div class=\"cadre-col\">\r\n        <div class=\"cadre-name\">${esc(label)}<\/div>\r\n        ${time ? `<div class=\"cadre-time\">${esc(time)}<\/div>` : ''}\r\n        ${items}\r\n      <\/div>`;\r\n  }).join('');\r\n}\r\n\r\n\/* ---------- Renderers: Resource Hub ---------- *\/\r\n\r\nlet RESOURCE_ROWS = null;\r\nlet activeButtonId = null;\r\nlet activeTypeFilter = null;     \/\/ value of typeCol to filter by, or null = All\r\n\r\nfunction renderResourceButtons() {\r\n  const bar = document.getElementById('resource-buttons');\r\n  bar.innerHTML = RESOURCE_BUTTONS.map(b =>\r\n    `<button class=\"resource-btn\" data-id=\"${esc(b.id)}\">${esc(b.label)}<\/button>`\r\n  ).join('');\r\n  bar.querySelectorAll('.resource-btn').forEach(btn => {\r\n    btn.addEventListener('click', () => activateButton(btn.dataset.id));\r\n  });\r\n}\r\n\r\nfunction activateButton(id) {\r\n  activeButtonId = id;\r\n  activeTypeFilter = null;     \/\/ reset side-filter on tab switch\r\n  document.querySelectorAll('.resource-btn').forEach(b => {\r\n    b.classList.toggle('active', b.dataset.id === id);\r\n  });\r\n  renderActivePanel();\r\n}\r\n\r\nfunction renderActivePanel() {\r\n  const wrap = document.getElementById('resource-panels');\r\n  const btn = RESOURCE_BUTTONS.find(b => b.id === activeButtonId);\r\n  if (!btn) { wrap.innerHTML = ''; return; }\r\n\r\n  \/\/ Data not connected\r\n  if (RESOURCE_ROWS === null) {\r\n    wrap.innerHTML = `\r\n      <div class=\"resource-panel active\">\r\n        <div class=\"panel-header\">\r\n          <div><div class=\"panel-title\">${esc(btn.panelTitle || btn.label)}<\/div><\/div>\r\n        <\/div>\r\n        <div class=\"state-msg\" style=\"text-align:left;padding:18px 0;\">\r\n          <strong>Resource data not yet connected.<\/strong><br>\r\n          Publish the \u201cForm Responses 1\u201d tab to the web as CSV and paste its URL into\r\n          <code>FORM_RESPONSES_CSV<\/code> at the top of the page script.\r\n        <\/div>\r\n      <\/div>`;\r\n    return;\r\n  }\r\n\r\n  \/\/ Filter the sheet's rows to just this button's set\r\n  const allRows = RESOURCE_ROWS.filter(btn.filter);\r\n\r\n  \/\/ Build the side-rail type list (unique non-empty values from typeCol)\r\n  const typeCounts = {};\r\n  allRows.forEach(r => {\r\n    const v = typeColValue(r, btn.typeCol);\r\n    if (v) typeCounts[v] = (typeCounts[v] || 0) + 1;\r\n  });\r\n  const sortedTypes = Object.keys(typeCounts).sort((a, b) => a.localeCompare(b));\r\n\r\n  \/\/ Apply any active type filter\r\n  const filteredRows = activeTypeFilter\r\n    ? allRows.filter(r => typeColValue(r, btn.typeCol) === activeTypeFilter)\r\n    : allRows;\r\n\r\n  wrap.innerHTML = `\r\n    <div class=\"resource-panel active\">\r\n      <div class=\"panel-header\">\r\n        <div>\r\n          <div class=\"panel-title\">${esc(btn.panelTitle || btn.label)}<\/div>\r\n          <div class=\"panel-count\" id=\"panel-count\"><\/div>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <div class=\"panel-search-bar\">\r\n        <label for=\"resource-search\" class=\"visually-hidden\">Search ${esc(btn.label)} resources<\/label>\r\n        <input id=\"resource-search\" type=\"search\"\r\n               placeholder=\"Search ${esc(btn.label.toLowerCase())} resources\u2026\"\r\n               aria-label=\"Search ${esc(btn.label)} resources\"\r\n               data-search>\r\n      <\/div>\r\n\r\n      <div class=\"panel-grid\">\r\n        <div>\r\n          <table class=\"resource-table\">\r\n            <thead>\r\n              <tr>\r\n                <th>Training Name<\/th>\r\n                <th>Type<\/th>\r\n              <\/tr>\r\n            <\/thead>\r\n            <tbody id=\"resource-tbody\"><\/tbody>\r\n          <\/table>\r\n          <div id=\"resource-empty\" class=\"state-msg\" style=\"display:none\">No matching resources.<\/div>\r\n          <div class=\"pagination\" style=\"display:none\">\r\n            <button class=\"page-btn prev\" aria-label=\"Previous page\">\u2039<\/button>\r\n            <span class=\"page-status\">Page 1<\/span>\r\n            <button class=\"page-btn next\" aria-label=\"Next page\">\u203a<\/button>\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <div class=\"sidebar\">\r\n          <aside class=\"type-filter\">\r\n            <h3>Filter by Type<\/h3>\r\n            ${sortedTypes.length\r\n              ? `<ul>\r\n                   <li><button class=\"type-btn ${activeTypeFilter === null ? 'active' : ''}\"\r\n                               data-type=\"\">All<span class=\"count\">${allRows.length}<\/span><\/button><\/li>\r\n                   ${sortedTypes.map(t => `\r\n                     <li><button class=\"type-btn ${activeTypeFilter === t ? 'active' : ''}\"\r\n                                 data-type=\"${esc(t)}\">${esc(formatTypeLabel(t, btn.id))}<span class=\"count\">${typeCounts[t]}<\/span><\/button><\/li>\r\n                   `).join('')}\r\n                 <\/ul>`\r\n              : `<div class=\"empty\">No types found for this group.<\/div>`}\r\n          <\/aside>\r\n          ${btn.actions && btn.actions.length ? `\r\n            <div class=\"panel-actions\">\r\n              ${btn.actions.slice(0, 2).map(a =>\r\n                `<a class=\"panel-action-btn\" href=\"${esc(a.url)}\" target=\"_blank\" rel=\"noopener noreferrer\">${esc(a.label)}<\/a>`\r\n              ).join('')}\r\n            <\/div>` : ''}\r\n        <\/div>\r\n      <\/div>\r\n    <\/div>`;\r\n\r\n  const tbody = document.getElementById('resource-tbody');\r\n  const emptyMsg = document.getElementById('resource-empty');\r\n  const countEl = document.getElementById('panel-count');\r\n  const searchInput = wrap.querySelector('input[data-search]');\r\n  const paginationEl = wrap.querySelector('.pagination');\r\n  const prevBtn = paginationEl?.querySelector('.prev');\r\n  const nextBtn = paginationEl?.querySelector('.next');\r\n  const statusEl = paginationEl?.querySelector('.page-status');\r\n\r\n  const PAGE_SIZE = 15;\r\n  let currentPage = 1;\r\n\r\n  function paint() {\r\n    const q = (searchInput?.value || '').toLowerCase();\r\n\r\n    const visible = filteredRows.filter(r => {\r\n      if (!q) return true;\r\n      const hay = (col(r, COL_NAME) + ' ' + typeColValue(r, btn.typeCol)).toLowerCase();\r\n      return hay.includes(q);\r\n    });\r\n\r\n    const totalPages = Math.max(1, Math.ceil(visible.length \/ PAGE_SIZE));\r\n    if (currentPage > totalPages) currentPage = totalPages;\r\n    if (currentPage < 1) currentPage = 1;\r\n    const start = (currentPage - 1) * PAGE_SIZE;\r\n    const paged = visible.slice(start, start + PAGE_SIZE);\r\n\r\n    tbody.innerHTML = paged.map(r => {\r\n      const name = col(r, COL_NAME);\r\n      const link = col(r, COL_LINK);\r\n      const type = typeColValue(r, btn.typeCol);\r\n      const cell = link\r\n        ? `<a href=\"${esc(link)}\" target=\"_blank\" rel=\"noopener noreferrer\">${esc(name || link)}<\/a>`\r\n        : esc(name);\r\n      return `<tr><td>${cell}<\/td><td class=\"type-cell\">${esc(formatTypeLabel(type, btn.id))}<\/td><\/tr>`;\r\n    }).join('');\r\n\r\n    emptyMsg.style.display = visible.length ? 'none' : 'block';\r\n    countEl.textContent = visible.length\r\n      + (visible.length === 1 ? ' resource' : ' resources')\r\n      + (activeTypeFilter ? ` \u00b7 filtered by \u201c${formatTypeLabel(activeTypeFilter, btn.id)}\u201d` : '');\r\n\r\n    if (paginationEl) {\r\n      if (visible.length <= PAGE_SIZE) {\r\n        paginationEl.style.display = 'none';\r\n      } else {\r\n        paginationEl.style.display = 'flex';\r\n        statusEl.textContent = `Page ${currentPage} of ${totalPages}`;\r\n        prevBtn.disabled = currentPage === 1;\r\n        nextBtn.disabled = currentPage === totalPages;\r\n      }\r\n    }\r\n  }\r\n\r\n  \/\/ Wire side-filter clicks\r\n  wrap.querySelectorAll('.type-btn').forEach(b => {\r\n    b.addEventListener('click', () => {\r\n      activeTypeFilter = b.dataset.type || null;\r\n      renderActivePanel();\r\n    });\r\n  });\r\n\r\n  if (searchInput) {\r\n    searchInput.addEventListener('input', () => { currentPage = 1; paint(); });\r\n  }\r\n  if (prevBtn) prevBtn.addEventListener('click', () => { currentPage--; paint(); });\r\n  if (nextBtn) nextBtn.addEventListener('click', () => { currentPage++; paint(); });\r\n\r\n  paint();\r\n}\r\n\r\n\/* ---------- Boot ---------- *\/\r\n\r\nasync function loadCsv(url) {\r\n  const res = await fetch(url);\r\n  if (!res.ok) throw new Error('HTTP ' + res.status);\r\n  return csvToTable(parseCSV(await res.text())).rows;\r\n}\r\n\r\n(async function init() {\r\n  renderResourceButtons();\r\n  if (RESOURCE_BUTTONS.length) activateButton(RESOURCE_BUTTONS[0].id);\r\n\r\n  try {\r\n    const linkedDates = await loadCsv(LINKED_DATES_CSV);\r\n    renderFlyers(linkedDates);\r\n    renderCadres(linkedDates);\r\n  } catch (err) {\r\n    document.getElementById('flyers-track').innerHTML =\r\n      '<div class=\"state-msg error\">Could not load conference data: ' + esc(err.message) + '<\/div>';\r\n    document.getElementById('cadre-grid').innerHTML =\r\n      '<div class=\"state-msg error\">Could not load cadre data.<\/div>';\r\n  }\r\n\r\n  if (FORM_RESPONSES_CSV) {\r\n    try {\r\n      RESOURCE_ROWS = await loadCsv(FORM_RESPONSES_CSV);\r\n      \/\/ Most-recent-first ordering by Timestamp (column A)\r\n      RESOURCE_ROWS.sort((a, b) => parseTimestamp(col(b,'A')) - parseTimestamp(col(a,'A')));\r\n      renderActivePanel();\r\n    } catch (err) {\r\n      RESOURCE_ROWS = [];\r\n      renderActivePanel();\r\n    }\r\n  }\r\n})();\r\n<\/script>\r\n\r\n<\/body>\r\n<\/html>\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-ead4377 e-grid e-con-boxed e-con e-parent\" data-id=\"ead4377\" data-element_type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>KVEC-SERTAC Upcoming Conferences &amp; Trainings Click any flyer to register. Scroll for more upcoming events. \u2039 \u203a Loading upcoming conferences\u2026 Cadre &amp; Network Schedules Loading cadre schedules\u2026 Library Resource Lists Choose a category to browse resources, filter the table by type, or search.<\/p>\n","protected":false},"author":2,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-19","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/kvecsertac.theholler.org\/index.php?rest_route=\/wp\/v2\/pages\/19","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/kvecsertac.theholler.org\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/kvecsertac.theholler.org\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/kvecsertac.theholler.org\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/kvecsertac.theholler.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=19"}],"version-history":[{"count":151,"href":"https:\/\/kvecsertac.theholler.org\/index.php?rest_route=\/wp\/v2\/pages\/19\/revisions"}],"predecessor-version":[{"id":386,"href":"https:\/\/kvecsertac.theholler.org\/index.php?rest_route=\/wp\/v2\/pages\/19\/revisions\/386"}],"wp:attachment":[{"href":"https:\/\/kvecsertac.theholler.org\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=19"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}