The repo for Purrform's main BigCommerce store.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

41 feat diet builder (#43)

authored by

Rogerio Romao and committed by
GitHub
b0152599 2beb0cea

+1126 -6
+113 -5
assets/js/theme/custom/diet-builder.js
··· 84 84 'https://cdn11.bigcommerce.com/s-lh9wfk05w0/product_images/uploaded_images/twocatsgreen.png', 85 85 }; 86 86 87 + const HEALTH_CONDITIONS = [ 88 + 'Diabetes', 89 + 'Chin Acne', 90 + 'Inflammatory Bowel Disease (IBD)', 91 + 'Stage 1 CKD', 92 + 'Urinary Tract Conditions', 93 + 'Dental disease', 94 + 'Hyperthyroidism', 95 + 'Obesity', 96 + ]; 97 + 87 98 // ── Helpers ────────────────────────────────────────────────────────── 88 99 89 100 /** ··· 146 157 total: null, 147 158 kcal: null, 148 159 unwantedIngredients: [], 160 + healthConditions: [], 149 161 }; 150 162 151 163 // Product data (fetched from API) ··· 201 213 total: null, 202 214 kcal: null, 203 215 unwantedIngredients: [], 216 + healthConditions: [], 204 217 }; 205 218 this.products = { tubs: null, pouches: null }; 206 219 this.productPromise = null; ··· 232 245 defaultImage { 233 246 urlOriginal 234 247 } 248 + categories { 249 + edges { 250 + node { 251 + name 252 + } 253 + } 254 + } 235 255 customFields { 236 256 edges { 237 257 node { ··· 273 293 const { edges, pageInfo } = data.site.products; 274 294 275 295 edges.forEach(({ node }) => { 296 + const categories = node.categories.edges.map( 297 + (edge) => edge.node.name, 298 + ); 299 + 276 300 const customFields = {}; 277 301 node.customFields.edges.forEach(({ node: cf }) => { 278 302 if (cf.name === 'Ingredient') { ··· 285 309 } 286 310 }); 287 311 312 + // Only include food products (those with Ingredients) 313 + if (!customFields.Ingredients) return; 314 + 288 315 allProducts.push({ 289 316 entityId: node.entityId, 290 317 name: node.name, 291 318 path: node.path, 292 319 image: node.defaultImage?.urlOriginal || '', 293 320 customFields, 321 + categories, 294 322 }); 295 323 }); 296 324 ··· 662 690 className: 'diet-builder-btn--secondary', 663 691 onClick: () => { 664 692 this.state.unwantedIngredients = []; 665 - this.calculateAndShowResults(); 693 + this.renderHealthStep(flow); 666 694 }, 667 695 }, 668 696 'Skip', ··· 672 700 'button', 673 701 { 674 702 className: 'diet-builder-btn--primary', 675 - onClick: () => this.submitIngredients(), 703 + onClick: () => this.submitIngredients(flow), 676 704 }, 677 705 'Next', 678 706 ); ··· 683 711 this.renderStep('Any ingredients your cat dislikes?', content); 684 712 } 685 713 686 - submitIngredients() { 714 + submitIngredients(flow) { 687 715 const cards = document.querySelectorAll( 688 716 '.diet-builder-ingredients__card--selected', 689 717 ); 690 718 this.state.unwantedIngredients = [...cards].map( 691 719 (card) => card.textContent, 692 720 ); 693 - this.calculateAndShowResults(); 721 + this.renderHealthStep(flow); 694 722 } 695 723 696 724 goBackFromIngredients(flow) { ··· 703 731 } 704 732 } 705 733 706 - // ── Step 6: Results ────────────────────────────────────────────── 734 + // ── Step 6: Health Conditions ──────────────────────────────────── 735 + 736 + renderHealthStep(flow) { 737 + const content = el('div', { className: 'diet-builder-health' }); 738 + 739 + const grid = el('div', { 740 + className: 'diet-builder-health__grid', 741 + }); 742 + 743 + const selected = new Set(this.state.healthConditions); 744 + 745 + HEALTH_CONDITIONS.forEach((condition) => { 746 + const isSelected = selected.has(condition); 747 + const card = el( 748 + 'button', 749 + { 750 + className: `diet-builder-health__card${ 751 + isSelected ? ' diet-builder-health__card--selected' : '' 752 + }`, 753 + onClick: () => { 754 + card.classList.toggle( 755 + 'diet-builder-health__card--selected', 756 + ); 757 + }, 758 + }, 759 + condition, 760 + ); 761 + grid.appendChild(card); 762 + }); 763 + 764 + const buttonGroup = el('div', { 765 + className: 'diet-builder-health__buttons', 766 + }); 767 + 768 + const backBtn = el( 769 + 'button', 770 + { 771 + className: 'diet-builder-btn--secondary', 772 + onClick: () => this.renderIngredientsStep(flow), 773 + }, 774 + 'Back', 775 + ); 776 + 777 + const skipBtn = el( 778 + 'button', 779 + { 780 + className: 'diet-builder-btn--secondary', 781 + onClick: () => { 782 + this.state.healthConditions = []; 783 + this.calculateAndShowResults(); 784 + }, 785 + }, 786 + 'Skip', 787 + ); 788 + 789 + const nextBtn = el( 790 + 'button', 791 + { 792 + className: 'diet-builder-btn--primary', 793 + onClick: () => this.submitHealth(), 794 + }, 795 + 'Next', 796 + ); 797 + 798 + buttonGroup.append(backBtn, skipBtn, nextBtn); 799 + content.append(grid, buttonGroup); 800 + 801 + this.renderStep('Does your cat have any health conditions?', content); 802 + } 803 + 804 + submitHealth() { 805 + const cards = document.querySelectorAll( 806 + '.diet-builder-health__card--selected', 807 + ); 808 + this.state.healthConditions = [...cards].map( 809 + (card) => card.textContent, 810 + ); 811 + this.calculateAndShowResults(); 812 + } 813 + 814 + // ── Step 7: Results ────────────────────────────────────────────── 707 815 708 816 calculateAndShowResults() { 709 817 const { weight, activity, coef } = this.state;
+67
assets/scss/custom/pages/_calculators.scss
··· 1110 1110 } 1111 1111 } 1112 1112 1113 + // ── Health conditions step ─────────────────────────────────── 1114 + .diet-builder-health { 1115 + margin-top: 35px; 1116 + display: flex; 1117 + flex-direction: column; 1118 + align-items: center; 1119 + } 1120 + 1121 + .diet-builder-health__grid { 1122 + display: grid; 1123 + grid-template-columns: repeat(2, 1fr); 1124 + gap: 12px; 1125 + width: 100%; 1126 + max-width: 600px; 1127 + margin-bottom: 2rem; 1128 + 1129 + @include breakpoint('medium') { 1130 + grid-template-columns: repeat(3, 1fr); 1131 + } 1132 + 1133 + @include breakpoint('large') { 1134 + grid-template-columns: repeat(4, 1fr); 1135 + } 1136 + } 1137 + 1138 + .diet-builder-health__card { 1139 + background-color: $color-f-w; 1140 + border: 2px solid $color-p; 1141 + border-radius: 12px; 1142 + padding: 12px 8px; 1143 + display: flex; 1144 + align-items: center; 1145 + justify-content: center; 1146 + font-size: 14px; 1147 + font-weight: 600; 1148 + color: $color-p; 1149 + text-align: center; 1150 + cursor: pointer; 1151 + transition: all 0.2s ease; 1152 + 1153 + &:hover { 1154 + background-color: rgba(104, 125, 106, 0.1); 1155 + } 1156 + 1157 + &--selected { 1158 + background-color: $color-p; 1159 + color: $color-f-w; 1160 + border-color: $color-p; 1161 + 1162 + &:hover { 1163 + background-color: darken(#687d6a, 8%); 1164 + } 1165 + } 1166 + } 1167 + 1168 + .diet-builder-health__buttons { 1169 + display: flex; 1170 + justify-content: center; 1171 + gap: 15px; 1172 + flex-wrap: wrap; 1173 + 1174 + .diet-builder-btn--primary, 1175 + .diet-builder-btn--secondary { 1176 + margin: 0; 1177 + } 1178 + } 1179 + 1113 1180 // ── Results step ───────────────────────────────────────────── 1114 1181 .diet-builder-results { 1115 1182 width: 99.4vw;
+1 -1
config.json
··· 1 1 { 2 - "name": "diet builder test", 2 + "name": "diet builder test v2", 3 3 "version": "6.10.0", 4 4 "template_engine": "handlebars_v4", 5 5 "meta": {
+945
templates/pages/custom/page/feline-grimace-scale.html
··· 1 + {{#partial "page"}} 2 + <head> 3 + <meta charset="UTF-8"> 4 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 5 + <title>Feline Grimace Scale</title> 6 + <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet"> 7 + <style> 8 + :root { 9 + --bg: #f7f4f0; 10 + --card: #ffffff; 11 + --purple: #7b5ea7; 12 + --purple-light: #ede8f5; 13 + --purple-mid: #b39ddb; 14 + --accent: #e8a0bf; 15 + --text: #2c2240; 16 + --text-muted: #7a6e8a; 17 + --selected: #7b5ea7; 18 + --border: #e0d8ed; 19 + --score-low: #4caf87; 20 + --score-mid: #e8a020; 21 + --score-high: #e05555; 22 + --radius: 16px; 23 + --transition: 0.22s cubic-bezier(0.4,0,0.2,1); 24 + } 25 + 26 + *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 27 + 28 + body { 29 + font-family: 'DM Sans', sans-serif; 30 + background: var(--bg); 31 + color: var(--text); 32 + min-height: 100vh; 33 + padding: 40px 20px 60px; 34 + } 35 + 36 + body::before { 37 + content: ''; 38 + position: fixed; 39 + top: -120px; right: -120px; 40 + width: 420px; height: 420px; 41 + background: radial-gradient(circle, #c9b8e830 0%, transparent 70%); 42 + pointer-events: none; 43 + z-index: 0; 44 + } 45 + 46 + body::after { 47 + content: ''; 48 + position: fixed; 49 + bottom: -80px; left: -80px; 50 + width: 320px; height: 320px; 51 + background: radial-gradient(circle, #e8a0bf20 0%, transparent 70%); 52 + pointer-events: none; 53 + z-index: 0; 54 + } 55 + 56 + .wrapper { 57 + max-width: 820px; 58 + margin: 0 auto; 59 + position: relative; 60 + z-index: 1; 61 + } 62 + 63 + header { 64 + text-align: center; 65 + margin-bottom: 40px; 66 + animation: fadeDown 0.6s ease both; 67 + } 68 + 69 + .paw-icon { 70 + font-size: 2.4rem; 71 + margin-bottom: 8px; 72 + display: block; 73 + } 74 + 75 + header h1 { 76 + font-family: 'Playfair Display', serif; 77 + font-size: clamp(1.8rem, 5vw, 2.8rem); 78 + font-weight: 700; 79 + color: var(--purple); 80 + line-height: 1.1; 81 + letter-spacing: -0.5px; 82 + } 83 + 84 + header p { 85 + margin-top: 10px; 86 + color: var(--text-muted); 87 + font-size: 0.95rem; 88 + font-weight: 300; 89 + max-width: 480px; 90 + margin-inline: auto; 91 + line-height: 1.6; 92 + } 93 + 94 + .notice { 95 + background: #fff8e6; 96 + border: 1.5px solid #f5c842; 97 + border-radius: 10px; 98 + padding: 12px 18px; 99 + font-size: 0.82rem; 100 + color: #7a5c00; 101 + text-align: center; 102 + margin-bottom: 32px; 103 + line-height: 1.5; 104 + animation: fadeDown 0.7s ease both; 105 + } 106 + 107 + .instructions { 108 + background: var(--purple-light); 109 + border-radius: var(--radius); 110 + padding: 18px 22px; 111 + margin-bottom: 32px; 112 + font-size: 0.88rem; 113 + color: var(--text); 114 + line-height: 1.7; 115 + animation: fadeDown 0.75s ease both; 116 + } 117 + 118 + .instructions strong { color: var(--purple); } 119 + 120 + .instructions .wait-list { 121 + display: flex; 122 + gap: 16px; 123 + flex-wrap: wrap; 124 + margin-top: 8px; 125 + } 126 + 127 + .wait-list span { 128 + background: white; 129 + border-radius: 20px; 130 + padding: 4px 14px; 131 + font-size: 0.82rem; 132 + font-weight: 500; 133 + color: var(--purple); 134 + border: 1.5px solid var(--purple-mid); 135 + } 136 + 137 + /* Action Unit Cards */ 138 + .unit-card { 139 + background: var(--card); 140 + border-radius: var(--radius); 141 + border: 1.5px solid var(--border); 142 + margin-bottom: 24px; 143 + overflow: hidden; 144 + animation: fadeUp 0.5s ease both; 145 + box-shadow: 0 2px 20px #7b5ea708; 146 + transition: box-shadow var(--transition); 147 + } 148 + 149 + .unit-card:hover { box-shadow: 0 6px 32px #7b5ea714; } 150 + .unit-card:nth-child(1) { animation-delay: 0.1s; } 151 + .unit-card:nth-child(2) { animation-delay: 0.15s; } 152 + .unit-card:nth-child(3) { animation-delay: 0.2s; } 153 + .unit-card:nth-child(4) { animation-delay: 0.25s; } 154 + .unit-card:nth-child(5) { animation-delay: 0.3s; } 155 + 156 + .unit-header { 157 + background: var(--purple); 158 + color: white; 159 + padding: 14px 22px; 160 + display: flex; 161 + align-items: center; 162 + gap: 10px; 163 + } 164 + 165 + .unit-header h2 { 166 + font-family: 'DM Sans', sans-serif; 167 + font-size: 0.82rem; 168 + font-weight: 600; 169 + letter-spacing: 2px; 170 + text-transform: uppercase; 171 + } 172 + 173 + .unit-header .unit-score-badge { 174 + margin-left: auto; 175 + background: rgba(255,255,255,0.2); 176 + border-radius: 20px; 177 + padding: 4px 14px; 178 + font-size: 0.82rem; 179 + font-weight: 600; 180 + transition: background var(--transition); 181 + } 182 + 183 + .unit-header .unit-score-badge.selected { 184 + background: white; 185 + color: var(--purple); 186 + } 187 + 188 + .options-row { 189 + display: grid; 190 + grid-template-columns: repeat(3, 1fr); 191 + gap: 0; 192 + border-top: 1px solid var(--border); 193 + } 194 + 195 + .option-btn { 196 + position: relative; 197 + display: flex; 198 + flex-direction: column; 199 + align-items: center; 200 + padding: 24px 16px 20px; 201 + cursor: pointer; 202 + border: none; 203 + background: transparent; 204 + transition: background var(--transition); 205 + border-right: 1px solid var(--border); 206 + outline: none; 207 + text-align: center; 208 + -webkit-tap-highlight-color: transparent; 209 + } 210 + 211 + .option-btn:last-child { border-right: none; } 212 + 213 + .option-btn:hover { background: var(--purple-light); } 214 + 215 + .option-btn.selected { 216 + background: var(--purple-light); 217 + } 218 + 219 + .option-btn.selected::after { 220 + content: ''; 221 + position: absolute; 222 + inset: 0; 223 + border: 2.5px solid var(--purple); 224 + border-radius: 0; 225 + pointer-events: none; 226 + } 227 + 228 + .option-btn:first-child.selected::after { border-radius: 0; } 229 + 230 + .score-pill { 231 + width: 32px; height: 32px; 232 + border-radius: 50%; 233 + border: 2.5px solid var(--border); 234 + display: flex; align-items: center; justify-content: center; 235 + font-weight: 700; 236 + font-size: 0.9rem; 237 + color: var(--text-muted); 238 + margin-bottom: 14px; 239 + transition: all var(--transition); 240 + background: white; 241 + } 242 + 243 + .option-btn.selected .score-pill { 244 + border-color: var(--purple); 245 + background: var(--purple); 246 + color: white; 247 + transform: scale(1.1); 248 + } 249 + 250 + .cat-illustration { 251 + width: 110px; 252 + height: 90px; 253 + margin-bottom: 12px; 254 + } 255 + 256 + .option-label { 257 + font-size: 0.8rem; 258 + font-weight: 600; 259 + color: var(--text); 260 + line-height: 1.3; 261 + margin-bottom: 4px; 262 + } 263 + 264 + .option-sublabel { 265 + font-size: 0.72rem; 266 + color: var(--text-muted); 267 + line-height: 1.3; 268 + font-weight: 400; 269 + } 270 + 271 + /* Score Panel */ 272 + .score-panel { 273 + background: var(--card); 274 + border-radius: var(--radius); 275 + border: 1.5px solid var(--border); 276 + overflow: hidden; 277 + animation: fadeUp 0.5s 0.4s ease both; 278 + box-shadow: 0 2px 20px #7b5ea708; 279 + } 280 + 281 + .score-panel-header { 282 + background: var(--text); 283 + color: white; 284 + padding: 16px 22px; 285 + display: flex; 286 + align-items: center; 287 + gap: 10px; 288 + } 289 + 290 + .score-panel-header h2 { 291 + font-family: 'Playfair Display', serif; 292 + font-size: 1.1rem; 293 + font-weight: 600; 294 + } 295 + 296 + .score-breakdown { 297 + display: flex; 298 + padding: 20px 22px 0; 299 + gap: 0; 300 + flex-wrap: wrap; 301 + } 302 + 303 + .score-item { 304 + flex: 1; 305 + min-width: 100px; 306 + text-align: center; 307 + padding: 12px 8px; 308 + border-right: 1px solid var(--border); 309 + } 310 + 311 + .score-item:last-child { border-right: none; } 312 + 313 + .score-item-label { 314 + font-size: 0.7rem; 315 + font-weight: 600; 316 + letter-spacing: 1.5px; 317 + text-transform: uppercase; 318 + color: var(--text-muted); 319 + margin-bottom: 6px; 320 + } 321 + 322 + .score-item-value { 323 + font-family: 'Playfair Display', serif; 324 + font-size: 1.6rem; 325 + font-weight: 700; 326 + color: var(--text); 327 + transition: all var(--transition); 328 + } 329 + 330 + .total-section { 331 + padding: 20px 22px; 332 + display: flex; 333 + align-items: center; 334 + gap: 24px; 335 + border-top: 1.5px solid var(--border); 336 + margin-top: 16px; 337 + flex-wrap: wrap; 338 + } 339 + 340 + .total-number { 341 + font-family: 'Playfair Display', serif; 342 + font-size: 4rem; 343 + font-weight: 700; 344 + line-height: 1; 345 + min-width: 80px; 346 + text-align: center; 347 + transition: color 0.4s ease; 348 + } 349 + 350 + .total-number.color-0 { color: var(--score-low); } 351 + .total-number.color-mild { color: var(--score-low); } 352 + .total-number.color-pain { color: var(--score-mid); } 353 + .total-number.color-severe { color: var(--score-high); } 354 + 355 + .total-out-of { 356 + font-size: 1rem; 357 + color: var(--text-muted); 358 + font-weight: 300; 359 + align-self: flex-end; 360 + padding-bottom: 8px; 361 + margin-left: -16px; 362 + } 363 + 364 + .interpretation { 365 + flex: 1; 366 + min-width: 200px; 367 + } 368 + 369 + .interp-badge { 370 + display: inline-block; 371 + border-radius: 20px; 372 + padding: 5px 16px; 373 + font-size: 0.75rem; 374 + font-weight: 700; 375 + letter-spacing: 1px; 376 + text-transform: uppercase; 377 + margin-bottom: 8px; 378 + transition: all 0.4s ease; 379 + } 380 + 381 + .badge-0, .badge-mild { background: #e8f5ef; color: #2d7a59; } 382 + .badge-pain { background: #fff3e0; color: #b56b00; } 383 + .badge-severe { background: #fde8e8; color: #c0392b; } 384 + 385 + .interp-text { 386 + font-size: 0.85rem; 387 + line-height: 1.6; 388 + color: var(--text); 389 + transition: opacity 0.3s ease; 390 + } 391 + 392 + .progress-bar-wrap { 393 + padding: 0 22px 22px; 394 + } 395 + 396 + .progress-label { 397 + display: flex; 398 + justify-content: space-between; 399 + font-size: 0.75rem; 400 + color: var(--text-muted); 401 + margin-bottom: 6px; 402 + font-weight: 500; 403 + } 404 + 405 + .progress-track { 406 + height: 8px; 407 + background: var(--border); 408 + border-radius: 20px; 409 + overflow: hidden; 410 + } 411 + 412 + .progress-fill { 413 + height: 100%; 414 + border-radius: 20px; 415 + width: 0%; 416 + transition: width 0.5s cubic-bezier(0.4,0,0.2,1), background 0.4s ease; 417 + background: var(--score-low); 418 + } 419 + 420 + .progress-fill.color-pain { background: var(--score-mid); } 421 + .progress-fill.color-severe { background: var(--score-high); } 422 + 423 + .cutoff-indicator { 424 + position: relative; 425 + height: 16px; 426 + margin-top: 4px; 427 + } 428 + 429 + .cutoff-line { 430 + position: absolute; 431 + left: 40%; 432 + top: 0; 433 + width: 2px; 434 + height: 14px; 435 + background: #c0392b; 436 + border-radius: 2px; 437 + } 438 + 439 + .cutoff-label { 440 + position: absolute; 441 + left: 40%; 442 + transform: translateX(-50%); 443 + top: 0; 444 + font-size: 0.65rem; 445 + color: #c0392b; 446 + font-weight: 600; 447 + white-space: nowrap; 448 + padding-left: 6px; 449 + } 450 + 451 + /* Animations */ 452 + @keyframes fadeDown { 453 + from { opacity: 0; transform: translateY(-16px); } 454 + to { opacity: 1; transform: translateY(0); } 455 + } 456 + 457 + @keyframes fadeUp { 458 + from { opacity: 0; transform: translateY(20px); } 459 + to { opacity: 1; transform: translateY(0); } 460 + } 461 + 462 + @keyframes pop { 463 + 0% { transform: scale(1); } 464 + 50% { transform: scale(1.15); } 465 + 100% { transform: scale(1); } 466 + } 467 + 468 + .pop-animate { animation: pop 0.3s ease; } 469 + 470 + /* SVG cat illustrations */ 471 + svg { display: block; } 472 + 473 + @media (max-width: 600px) { 474 + .options-row { grid-template-columns: 1fr; } 475 + .option-btn { border-right: none; border-bottom: 1px solid var(--border); flex-direction: row; gap: 14px; text-align: left; padding: 16px; } 476 + .option-btn:last-child { border-bottom: none; } 477 + .cat-illustration { width: 70px; height: 60px; margin-bottom: 0; flex-shrink: 0; } 478 + .score-breakdown { gap: 0; } 479 + .score-item { min-width: 60px; } 480 + .total-section { gap: 14px; } 481 + } 482 + </style> 483 + </head> 484 + <body> 485 + <div class="wrapper"> 486 + 487 + <header> 488 + <span class="paw-icon">🐾</span> 489 + <h1>Feline Grimace Scale</h1> 490 + <p>Observe the cat undisturbed from a distance for 30 seconds, then score each action unit below.</p> 491 + </header> 492 + 493 + <div class="notice"> 494 + ⚠️ <strong>For owners:</strong> Always consult your vet before administering any medication. Some human pain killers can be fatal to cats. 495 + </div> 496 + 497 + <div class="instructions"> 498 + <strong>Wait until the cat has finished before scoring:</strong> 499 + <div class="wait-list"> 500 + <span>🧹 Grooming</span> 501 + <span>😴 Sleeping</span> 502 + <span>🎾 Playing</span> 503 + <span>🍽️ Eating</span> 504 + </div> 505 + </div> 506 + 507 + <div id="units-container"></div> 508 + 509 + <div class="score-panel"> 510 + <div class="score-panel-header"> 511 + <span>📊</span> 512 + <h2>Your Scores</h2> 513 + </div> 514 + <div class="score-breakdown" id="score-breakdown"></div> 515 + <div class="total-section"> 516 + <div> 517 + <div style="font-size:0.72rem;font-weight:600;letter-spacing:1.5px;text-transform:uppercase;color:var(--text-muted);margin-bottom:4px;">Total</div> 518 + <div style="display:flex;align-items:flex-end;gap:4px;"> 519 + <div class="total-number color-0" id="total-display">0</div> 520 + <div class="total-out-of">/ 10</div> 521 + </div> 522 + </div> 523 + <div class="interpretation"> 524 + <div class="interp-badge badge-0" id="interp-badge">No Pain</div> 525 + <div class="interp-text" id="interp-text">This cat is not in pain. If you are a cat owner and are concerned, please consult your veterinary surgeon.</div> 526 + </div> 527 + </div> 528 + <div class="progress-bar-wrap"> 529 + <div class="progress-label"> 530 + <span>0</span> 531 + <span>Rescue analgesia threshold: 4</span> 532 + <span>10</span> 533 + </div> 534 + <div class="progress-track"> 535 + <div class="progress-fill" id="progress-fill"></div> 536 + </div> 537 + <div class="cutoff-indicator"> 538 + <div class="cutoff-line"></div> 539 + <span class="cutoff-label">4</span> 540 + </div> 541 + </div> 542 + </div> 543 + 544 + </div> 545 + 546 + <script> 547 + const units = [ 548 + { 549 + id: 'ears', 550 + label: 'Ear Position', 551 + options: [ 552 + { 553 + score: 0, label: 'Ears facing forward', sublabel: 'Absent', 554 + svg: `<svg viewBox="0 0 110 90" fill="none" xmlns="http://www.w3.org/2000/svg"> 555 + <ellipse cx="55" cy="55" rx="30" ry="26" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 556 + <polygon points="32,36 22,14 44,30" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5" stroke-linejoin="round"/> 557 + <polygon points="78,36 88,14 66,30" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5" stroke-linejoin="round"/> 558 + <circle cx="44" cy="53" r="5" fill="#7b5ea7" opacity="0.7"/> 559 + <circle cx="66" cy="53" r="5" fill="#7b5ea7" opacity="0.7"/> 560 + <path d="M47 66 Q55 72 63 66" stroke="#7b5ea7" stroke-width="1.5" fill="none" stroke-linecap="round"/> 561 + <line x1="38" y1="56" x2="24" y2="52" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 562 + <line x1="38" y1="59" x2="23" y2="59" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 563 + <line x1="72" y1="56" x2="86" y2="52" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 564 + <line x1="72" y1="59" x2="87" y2="59" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 565 + </svg>` 566 + }, 567 + { 568 + score: 1, label: 'Ears slightly pulled apart', sublabel: 'Moderately present or uncertain', 569 + svg: `<svg viewBox="0 0 110 90" fill="none" xmlns="http://www.w3.org/2000/svg"> 570 + <ellipse cx="55" cy="55" rx="30" ry="26" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 571 + <polygon points="32,36 16,18 40,32" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5" stroke-linejoin="round"/> 572 + <polygon points="78,36 94,18 70,32" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5" stroke-linejoin="round"/> 573 + <circle cx="44" cy="53" r="5" fill="#7b5ea7" opacity="0.7"/> 574 + <circle cx="66" cy="53" r="5" fill="#7b5ea7" opacity="0.7"/> 575 + <path d="M47 66 Q55 71 63 66" stroke="#7b5ea7" stroke-width="1.5" fill="none" stroke-linecap="round"/> 576 + <line x1="38" y1="56" x2="24" y2="52" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 577 + <line x1="38" y1="59" x2="23" y2="59" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 578 + <line x1="72" y1="56" x2="86" y2="52" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 579 + <line x1="72" y1="59" x2="87" y2="59" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 580 + </svg>` 581 + }, 582 + { 583 + score: 2, label: 'Ears rotated outwards', sublabel: 'Markedly present', 584 + svg: `<svg viewBox="0 0 110 90" fill="none" xmlns="http://www.w3.org/2000/svg"> 585 + <ellipse cx="55" cy="55" rx="30" ry="26" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 586 + <polygon points="30,38 10,30 36,34" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5" stroke-linejoin="round"/> 587 + <polygon points="80,38 100,30 74,34" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5" stroke-linejoin="round"/> 588 + <circle cx="44" cy="53" r="5" fill="#7b5ea7" opacity="0.7"/> 589 + <circle cx="66" cy="53" r="5" fill="#7b5ea7" opacity="0.7"/> 590 + <path d="M47 66 Q55 70 63 66" stroke="#7b5ea7" stroke-width="1.5" fill="none" stroke-linecap="round"/> 591 + <line x1="38" y1="56" x2="24" y2="52" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 592 + <line x1="38" y1="59" x2="23" y2="59" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 593 + <line x1="72" y1="56" x2="86" y2="52" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 594 + <line x1="72" y1="59" x2="87" y2="59" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 595 + </svg>` 596 + } 597 + ] 598 + }, 599 + { 600 + id: 'eyes', 601 + label: 'Orbital Tightening', 602 + options: [ 603 + { 604 + score: 0, label: 'Eyes opened', sublabel: 'Absent', 605 + svg: `<svg viewBox="0 0 110 90" fill="none" xmlns="http://www.w3.org/2000/svg"> 606 + <ellipse cx="55" cy="52" rx="30" ry="26" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 607 + <polygon points="32,34 22,14 44,30" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 608 + <polygon points="78,34 88,14 66,30" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 609 + <ellipse cx="42" cy="50" rx="8" ry="8" fill="white" stroke="#7b5ea7" stroke-width="1.5"/> 610 + <ellipse cx="68" cy="50" rx="8" ry="8" fill="white" stroke="#7b5ea7" stroke-width="1.5"/> 611 + <circle cx="42" cy="50" r="4" fill="#3a2a5a"/> 612 + <circle cx="68" cy="50" r="4" fill="#3a2a5a"/> 613 + <circle cx="44" cy="48" r="1.5" fill="white"/> 614 + <circle cx="70" cy="48" r="1.5" fill="white"/> 615 + <path d="M47 65 Q55 70 63 65" stroke="#7b5ea7" stroke-width="1.5" fill="none" stroke-linecap="round"/> 616 + </svg>` 617 + }, 618 + { 619 + score: 1, label: 'Partially closed eyes', sublabel: 'Moderately present or uncertain', 620 + svg: `<svg viewBox="0 0 110 90" fill="none" xmlns="http://www.w3.org/2000/svg"> 621 + <ellipse cx="55" cy="52" rx="30" ry="26" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 622 + <polygon points="32,34 22,14 44,30" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 623 + <polygon points="78,34 88,14 66,30" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 624 + <ellipse cx="42" cy="50" rx="8" ry="5.5" fill="white" stroke="#7b5ea7" stroke-width="1.5"/> 625 + <ellipse cx="68" cy="50" rx="8" ry="5.5" fill="white" stroke="#7b5ea7" stroke-width="1.5"/> 626 + <circle cx="42" cy="51" r="3.5" fill="#3a2a5a"/> 627 + <circle cx="68" cy="51" r="3.5" fill="#3a2a5a"/> 628 + <line x1="34" y1="45" x2="50" y2="45" stroke="#7b5ea7" stroke-width="1.8" stroke-linecap="round"/> 629 + <line x1="60" y1="45" x2="76" y2="45" stroke="#7b5ea7" stroke-width="1.8" stroke-linecap="round"/> 630 + <path d="M47 65 Q55 70 63 65" stroke="#7b5ea7" stroke-width="1.5" fill="none" stroke-linecap="round"/> 631 + </svg>` 632 + }, 633 + { 634 + score: 2, label: 'Squinted eyes', sublabel: 'Markedly present', 635 + svg: `<svg viewBox="0 0 110 90" fill="none" xmlns="http://www.w3.org/2000/svg"> 636 + <ellipse cx="55" cy="52" rx="30" ry="26" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 637 + <polygon points="32,34 22,14 44,30" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 638 + <polygon points="78,34 88,14 66,30" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 639 + <ellipse cx="42" cy="52" rx="8" ry="3" fill="white" stroke="#7b5ea7" stroke-width="1.5"/> 640 + <ellipse cx="68" cy="52" rx="8" ry="3" fill="white" stroke="#7b5ea7" stroke-width="1.5"/> 641 + <circle cx="42" cy="52" r="2" fill="#3a2a5a"/> 642 + <circle cx="68" cy="52" r="2" fill="#3a2a5a"/> 643 + <line x1="34" y1="49" x2="50" y2="49" stroke="#7b5ea7" stroke-width="2" stroke-linecap="round"/> 644 + <line x1="60" y1="49" x2="76" y2="49" stroke="#7b5ea7" stroke-width="2" stroke-linecap="round"/> 645 + <path d="M47 66 Q55 70 63 66" stroke="#7b5ea7" stroke-width="1.5" fill="none" stroke-linecap="round"/> 646 + </svg>` 647 + } 648 + ] 649 + }, 650 + { 651 + id: 'muzzle', 652 + label: 'Muzzle Tension', 653 + options: [ 654 + { 655 + score: 0, label: 'Relaxed (round shape)', sublabel: 'Absent', 656 + svg: `<svg viewBox="0 0 110 90" fill="none" xmlns="http://www.w3.org/2000/svg"> 657 + <ellipse cx="55" cy="52" rx="30" ry="26" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 658 + <polygon points="32,34 22,14 44,30" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 659 + <polygon points="78,34 88,14 66,30" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 660 + <circle cx="44" cy="50" r="4.5" fill="#7b5ea7" opacity="0.6"/> 661 + <circle cx="66" cy="50" r="4.5" fill="#7b5ea7" opacity="0.6"/> 662 + <ellipse cx="55" cy="62" rx="12" ry="10" fill="white" stroke="#7b5ea7" stroke-width="1.5"/> 663 + <ellipse cx="55" cy="62" rx="8" ry="7" fill="#f0eaf8"/> 664 + <path d="M47 67 Q55 73 63 67" stroke="#7b5ea7" stroke-width="1.5" fill="none" stroke-linecap="round"/> 665 + </svg>` 666 + }, 667 + { 668 + score: 1, label: 'Mild tension', sublabel: 'Moderately present or uncertain', 669 + svg: `<svg viewBox="0 0 110 90" fill="none" xmlns="http://www.w3.org/2000/svg"> 670 + <ellipse cx="55" cy="52" rx="30" ry="26" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 671 + <polygon points="32,34 22,14 44,30" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 672 + <polygon points="78,34 88,14 66,30" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 673 + <circle cx="44" cy="50" r="4.5" fill="#7b5ea7" opacity="0.6"/> 674 + <circle cx="66" cy="50" r="4.5" fill="#7b5ea7" opacity="0.6"/> 675 + <ellipse cx="55" cy="62" rx="11" ry="8.5" fill="white" stroke="#7b5ea7" stroke-width="1.5"/> 676 + <path d="M47 67 Q55 72 63 67" stroke="#7b5ea7" stroke-width="1.5" fill="none" stroke-linecap="round"/> 677 + </svg>` 678 + }, 679 + { 680 + score: 2, label: 'Tense (elliptical shape)', sublabel: 'Markedly present', 681 + svg: `<svg viewBox="0 0 110 90" fill="none" xmlns="http://www.w3.org/2000/svg"> 682 + <ellipse cx="55" cy="52" rx="30" ry="26" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 683 + <polygon points="32,34 22,14 44,30" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 684 + <polygon points="78,34 88,14 66,30" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 685 + <circle cx="44" cy="50" r="4.5" fill="#7b5ea7" opacity="0.6"/> 686 + <circle cx="66" cy="50" r="4.5" fill="#7b5ea7" opacity="0.6"/> 687 + <ellipse cx="55" cy="62" rx="14" ry="6" fill="white" stroke="#7b5ea7" stroke-width="1.5"/> 688 + <path d="M47 65 Q55 68 63 65" stroke="#7b5ea7" stroke-width="1.5" fill="none" stroke-linecap="round"/> 689 + </svg>` 690 + } 691 + ] 692 + }, 693 + { 694 + id: 'whiskers', 695 + label: 'Whiskers Change', 696 + options: [ 697 + { 698 + score: 0, label: 'Loose and curved', sublabel: 'Absent', 699 + svg: `<svg viewBox="0 0 110 90" fill="none" xmlns="http://www.w3.org/2000/svg"> 700 + <ellipse cx="55" cy="52" rx="28" ry="24" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 701 + <polygon points="33,36 24,16 44,32" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 702 + <polygon points="77,36 86,16 66,32" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 703 + <circle cx="44" cy="51" r="4" fill="#7b5ea7" opacity="0.6"/> 704 + <circle cx="66" cy="51" r="4" fill="#7b5ea7" opacity="0.6"/> 705 + <path d="M47 64 Q55 69 63 64" stroke="#7b5ea7" stroke-width="1.5" fill="none" stroke-linecap="round"/> 706 + <!-- curved whiskers --> 707 + <path d="M38 53 Q22 50 10 53" stroke="#7b5ea7" stroke-width="1.2" fill="none" stroke-linecap="round"/> 708 + <path d="M38 57 Q22 57 10 60" stroke="#7b5ea7" stroke-width="1.2" fill="none" stroke-linecap="round"/> 709 + <path d="M38 61 Q22 64 10 68" stroke="#7b5ea7" stroke-width="1.2" fill="none" stroke-linecap="round"/> 710 + <path d="M72 53 Q88 50 100 53" stroke="#7b5ea7" stroke-width="1.2" fill="none" stroke-linecap="round"/> 711 + <path d="M72 57 Q88 57 100 60" stroke="#7b5ea7" stroke-width="1.2" fill="none" stroke-linecap="round"/> 712 + <path d="M72 61 Q88 64 100 68" stroke="#7b5ea7" stroke-width="1.2" fill="none" stroke-linecap="round"/> 713 + </svg>` 714 + }, 715 + { 716 + score: 1, label: 'Slightly curved or straight (closer together)', sublabel: 'Moderately present or uncertain', 717 + svg: `<svg viewBox="0 0 110 90" fill="none" xmlns="http://www.w3.org/2000/svg"> 718 + <ellipse cx="55" cy="52" rx="28" ry="24" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 719 + <polygon points="33,36 24,16 44,32" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 720 + <polygon points="77,36 86,16 66,32" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 721 + <circle cx="44" cy="51" r="4" fill="#7b5ea7" opacity="0.6"/> 722 + <circle cx="66" cy="51" r="4" fill="#7b5ea7" opacity="0.6"/> 723 + <path d="M47 64 Q55 69 63 64" stroke="#7b5ea7" stroke-width="1.5" fill="none" stroke-linecap="round"/> 724 + <!-- straighter whiskers --> 725 + <line x1="38" y1="54" x2="10" y2="54" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 726 + <line x1="38" y1="58" x2="10" y2="58" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 727 + <line x1="38" y1="62" x2="10" y2="63" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 728 + <line x1="72" y1="54" x2="100" y2="54" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 729 + <line x1="72" y1="58" x2="100" y2="58" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 730 + <line x1="72" y1="62" x2="100" y2="63" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 731 + </svg>` 732 + }, 733 + { 734 + score: 2, label: 'Straight and moving forward', sublabel: 'Markedly present', 735 + svg: `<svg viewBox="0 0 110 90" fill="none" xmlns="http://www.w3.org/2000/svg"> 736 + <ellipse cx="55" cy="52" rx="28" ry="24" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 737 + <polygon points="33,36 24,16 44,32" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 738 + <polygon points="77,36 86,16 66,32" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 739 + <circle cx="44" cy="51" r="4" fill="#7b5ea7" opacity="0.6"/> 740 + <circle cx="66" cy="51" r="4" fill="#7b5ea7" opacity="0.6"/> 741 + <path d="M47 64 Q55 69 63 64" stroke="#7b5ea7" stroke-width="1.5" fill="none" stroke-linecap="round"/> 742 + <!-- forward-pointing whiskers --> 743 + <line x1="38" y1="55" x2="6" y2="51" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 744 + <line x1="38" y1="58" x2="6" y2="58" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 745 + <line x1="38" y1="61" x2="6" y2="65" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 746 + <line x1="72" y1="55" x2="104" y2="51" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 747 + <line x1="72" y1="58" x2="104" y2="58" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 748 + <line x1="72" y1="61" x2="104" y2="65" stroke="#7b5ea7" stroke-width="1.2" stroke-linecap="round"/> 749 + </svg>` 750 + } 751 + ] 752 + }, 753 + { 754 + id: 'head', 755 + label: 'Head Position', 756 + options: [ 757 + { 758 + score: 0, label: 'Head above the shoulder line', sublabel: 'Absent', 759 + svg: `<svg viewBox="0 0 110 90" fill="none" xmlns="http://www.w3.org/2000/svg"> 760 + <!-- body --> 761 + <ellipse cx="55" cy="72" rx="32" ry="16" fill="#e8e0f4" stroke="#7b5ea7" stroke-width="1.5"/> 762 + <!-- head high --> 763 + <ellipse cx="55" cy="40" rx="22" ry="20" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 764 + <polygon points="40,26 32,10 50,24" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 765 + <polygon points="70,26 78,10 60,24" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 766 + <circle cx="46" cy="40" r="4" fill="#7b5ea7" opacity="0.6"/> 767 + <circle cx="64" cy="40" r="4" fill="#7b5ea7" opacity="0.6"/> 768 + <!-- shoulder line dashes --> 769 + <line x1="10" y1="58" x2="100" y2="58" stroke="#aaa" stroke-width="1" stroke-dasharray="4,3"/> 770 + </svg>` 771 + }, 772 + { 773 + score: 1, label: 'Head aligned with the shoulder line', sublabel: 'Moderately present or uncertain', 774 + svg: `<svg viewBox="0 0 110 90" fill="none" xmlns="http://www.w3.org/2000/svg"> 775 + <ellipse cx="55" cy="72" rx="32" ry="16" fill="#e8e0f4" stroke="#7b5ea7" stroke-width="1.5"/> 776 + <!-- head level --> 777 + <ellipse cx="55" cy="52" rx="22" ry="20" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 778 + <polygon points="40,38 32,20 50,36" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 779 + <polygon points="70,38 78,20 60,36" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 780 + <circle cx="46" cy="52" r="4" fill="#7b5ea7" opacity="0.6"/> 781 + <circle cx="64" cy="52" r="4" fill="#7b5ea7" opacity="0.6"/> 782 + <line x1="10" y1="58" x2="100" y2="58" stroke="#aaa" stroke-width="1" stroke-dasharray="4,3"/> 783 + </svg>` 784 + }, 785 + { 786 + score: 2, label: 'Head below the shoulder line or tilted down', sublabel: 'Markedly present', 787 + svg: `<svg viewBox="0 0 110 90" fill="none" xmlns="http://www.w3.org/2000/svg"> 788 + <ellipse cx="55" cy="72" rx="32" ry="14" fill="#e8e0f4" stroke="#7b5ea7" stroke-width="1.5"/> 789 + <!-- head low/down --> 790 + <ellipse cx="55" cy="66" rx="22" ry="18" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 791 + <polygon points="40,52 33,34 50,50" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 792 + <polygon points="70,52 77,34 60,50" fill="#f0eaf8" stroke="#7b5ea7" stroke-width="1.5"/> 793 + <circle cx="46" cy="65" r="4" fill="#7b5ea7" opacity="0.6"/> 794 + <circle cx="64" cy="65" r="4" fill="#7b5ea7" opacity="0.6"/> 795 + <line x1="10" y1="58" x2="100" y2="58" stroke="#aaa" stroke-width="1" stroke-dasharray="4,3"/> 796 + </svg>` 797 + } 798 + ] 799 + } 800 + ]; 801 + 802 + const scores = { ears: null, eyes: null, muzzle: null, whiskers: null, head: null }; 803 + const unitIds = ['ears', 'eyes', 'muzzle', 'whiskers', 'head']; 804 + 805 + function getTotal() { 806 + return unitIds.reduce((sum, id) => sum + (scores[id] ?? 0), 0); 807 + } 808 + 809 + function getColorClass(total) { 810 + if (total === 0) return 'color-0'; 811 + if (total <= 3) return 'color-mild'; 812 + if (total <= 8) return 'color-pain'; 813 + return 'color-severe'; 814 + } 815 + 816 + function getBadgeClass(total) { 817 + if (total === 0) return 'badge-0'; 818 + if (total <= 3) return 'badge-mild'; 819 + if (total <= 8) return 'badge-pain'; 820 + return 'badge-severe'; 821 + } 822 + 823 + function getBadgeLabel(total) { 824 + if (total === 0) return 'No Pain'; 825 + if (total <= 3) return 'Mild / No Pain'; 826 + if (total <= 8) return 'Likely In Pain'; 827 + return 'Severe Pain'; 828 + } 829 + 830 + function getInterpText(total) { 831 + if (total === 0) return 'This cat is not in pain. If you are a cat owner and are concerned, please consult your veterinary surgeon.'; 832 + if (total <= 3) return 'This cat is not in pain or has mild pain. Pain should be re-evaluated at regular intervals since FGS scores could increase, and the cat might require analgesics.'; 833 + if (total <= 8) return 'This cat is likely to be in pain. This score indicates the need for additional analgesia. This decision should be made by a veterinary surgeon based on clinical judgement. If in doubt, reassess in 10–15 minutes.'; 834 + return 'This cat is likely to be in severe pain. This score indicates the need for additional analgesia. This decision should be made by a veterinary surgeon. If in doubt, reassess in 10–15 minutes.'; 835 + } 836 + 837 + function updateScorePanel() { 838 + const total = getTotal(); 839 + const colorClass = getColorClass(total); 840 + 841 + const totalEl = document.getElementById('total-display'); 842 + totalEl.textContent = total; 843 + totalEl.className = `total-number ${colorClass}`; 844 + totalEl.classList.add('pop-animate'); 845 + totalEl.addEventListener('animationend', () => totalEl.classList.remove('pop-animate'), { once: true }); 846 + 847 + const badge = document.getElementById('interp-badge'); 848 + badge.className = `interp-badge ${getBadgeClass(total)}`; 849 + badge.textContent = getBadgeLabel(total); 850 + 851 + document.getElementById('interp-text').textContent = getInterpText(total); 852 + 853 + const fill = document.getElementById('progress-fill'); 854 + const pct = (total / 10) * 100; 855 + fill.style.width = pct + '%'; 856 + fill.className = `progress-fill ${colorClass}`; 857 + 858 + // update breakdown 859 + unitIds.forEach(id => { 860 + const el = document.getElementById(`score-val-${id}`); 861 + if (el) { 862 + el.textContent = scores[id] !== null ? scores[id] : '—'; 863 + el.style.color = scores[id] !== null ? 'var(--purple)' : 'var(--text-muted)'; 864 + } 865 + }); 866 + 867 + // update unit score badges 868 + unitIds.forEach(id => { 869 + const badge = document.getElementById(`unit-badge-${id}`); 870 + if (badge) { 871 + if (scores[id] !== null) { 872 + badge.textContent = `Score: ${scores[id]}`; 873 + badge.classList.add('selected'); 874 + } else { 875 + badge.textContent = 'Not scored'; 876 + badge.classList.remove('selected'); 877 + } 878 + } 879 + }); 880 + } 881 + 882 + function buildUI() { 883 + const container = document.getElementById('units-container'); 884 + const breakdown = document.getElementById('score-breakdown'); 885 + 886 + // Build breakdown items 887 + const labels = ['Ears', 'Eyes', 'Muzzle', 'Whiskers', 'Head']; 888 + unitIds.forEach((id, i) => { 889 + const item = document.createElement('div'); 890 + item.className = 'score-item'; 891 + item.innerHTML = `<div class="score-item-label">${labels[i]}</div><div class="score-item-value" id="score-val-${id}">—</div>`; 892 + breakdown.appendChild(item); 893 + }); 894 + 895 + // Build unit cards 896 + units.forEach(unit => { 897 + const card = document.createElement('div'); 898 + card.className = 'unit-card'; 899 + card.innerHTML = ` 900 + <div class="unit-header"> 901 + <h2>${unit.label}</h2> 902 + <span class="unit-score-badge" id="unit-badge-${unit.id}">Not scored</span> 903 + </div> 904 + <div class="options-row" role="group" aria-label="${unit.label}"> 905 + ${unit.options.map(opt => ` 906 + <button class="option-btn" data-unit="${unit.id}" data-score="${opt.score}" aria-pressed="false"> 907 + <div class="score-pill">${opt.score}</div> 908 + <div class="cat-illustration">${opt.svg}</div> 909 + <div class="option-label">${opt.label}</div> 910 + <div class="option-sublabel">${opt.sublabel}</div> 911 + </button> 912 + `).join('')} 913 + </div> 914 + `; 915 + container.appendChild(card); 916 + }); 917 + 918 + // Event delegation 919 + container.addEventListener('click', e => { 920 + const btn = e.target.closest('.option-btn'); 921 + if (!btn) return; 922 + const unitId = btn.dataset.unit; 923 + const score = parseInt(btn.dataset.score); 924 + 925 + // deselect siblings 926 + const siblings = btn.closest('.options-row').querySelectorAll('.option-btn'); 927 + siblings.forEach(s => { s.classList.remove('selected'); s.setAttribute('aria-pressed', 'false'); }); 928 + 929 + // select this 930 + btn.classList.add('selected'); 931 + btn.setAttribute('aria-pressed', 'true'); 932 + scores[unitId] = score; 933 + 934 + updateScorePanel(); 935 + }); 936 + } 937 + 938 + buildUI(); 939 + updateScorePanel(); 940 + </script> 941 + </body> 942 + </html> 943 + 944 + {{/partial}} 945 + {{> layout/base}}