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

Configure Feed

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

Refactor Feline Grimace Scale page layout and styles (#49)

authored by

Rogerio Romao and committed by
GitHub
476c742c 31af466b

+937 -786
+2
assets/js/app.js
··· 55 55 const customClasses = { 56 56 'pages/custom/page/diet-builder': () => 57 57 import('./theme/custom/diet-builder'), 58 + 'pages/custom/page/feline-grimace-scale': () => 59 + import('./theme/custom/feline-grimace-scale'), 58 60 }; 59 61 60 62 /**
+237
assets/js/theme/custom/feline-grimace-scale.js
··· 1 + import PageManager from '../page-manager'; 2 + 3 + // ── Data ───────────────────────────────────────────────────────────── 4 + 5 + const units = [ 6 + { 7 + id: 'ears', 8 + label: 'Ear Position', 9 + options: [ 10 + { 11 + score: 0, label: 'Ears facing forward', sublabel: 'Absent', 12 + img: '<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-a-ear-position-0.jpg?t=1771863537" alt="Ear position - absent" loading="lazy">', 13 + }, 14 + { 15 + score: 1, label: 'Ears slightly pulled apart', sublabel: 'Moderately present or uncertain', 16 + img: '<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-a-ear-position-1.jpg?t=1771863537" alt="Ear position - moderately present" loading="lazy">', 17 + }, 18 + { 19 + score: 2, label: 'Ears rotated outwards', sublabel: 'Markedly present', 20 + img: '<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-a-ear-position-2.jpg?t=1771863538" alt="Ear position - markedly present" loading="lazy">', 21 + }, 22 + ], 23 + }, 24 + { 25 + id: 'eyes', 26 + label: 'Orbital Tightening', 27 + options: [ 28 + { 29 + score: 0, label: 'Eyes opened', sublabel: 'Absent', 30 + img: '<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-b-orbital-tightening-0.jpg?t=1771863539" alt="Orbital tightening - absent" loading="lazy">', 31 + }, 32 + { 33 + score: 1, label: 'Partially closed eyes', sublabel: 'Moderately present or uncertain', 34 + img: '<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-b-orbital-tightening-1.jpg?t=1771863539" alt="Orbital tightening - moderately present" loading="lazy">', 35 + }, 36 + { 37 + score: 2, label: 'Squinted eyes', sublabel: 'Markedly present', 38 + img: '<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-b-orbital-tightening-2.jpg?t=1771863540" alt="Orbital tightening - markedly present" loading="lazy">', 39 + }, 40 + ], 41 + }, 42 + { 43 + id: 'muzzle', 44 + label: 'Muzzle Tension', 45 + options: [ 46 + { 47 + score: 0, label: 'Relaxed (round shape)', sublabel: 'Absent', 48 + img: '<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-c-muzzle-tension-0.jpg?t=1771863541" alt="Muzzle tension - absent" loading="lazy">', 49 + }, 50 + { 51 + score: 1, label: 'Mild tension', sublabel: 'Moderately present or uncertain', 52 + img: '<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-c-muzzle-tension-1.jpg?t=1771863542" alt="Muzzle tension - moderately present" loading="lazy">', 53 + }, 54 + { 55 + score: 2, label: 'Tense (elliptical shape)', sublabel: 'Markedly present', 56 + img: '<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-c-muzzle-tension-2.jpg?t=1771863542" alt="Muzzle tension - markedly present" loading="lazy">', 57 + }, 58 + ], 59 + }, 60 + { 61 + id: 'whiskers', 62 + label: 'Whiskers Change', 63 + options: [ 64 + { 65 + score: 0, label: 'Loose and curved', sublabel: 'Absent', 66 + img: '<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-d-whiskers-change-0.jpg?t=1771863543" alt="Whiskers change - absent" loading="lazy">', 67 + }, 68 + { 69 + score: 1, label: 'Slightly curved or straight (closer together)', sublabel: 'Moderately present or uncertain', 70 + img: '<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-d-whiskers-change-1.jpg?t=1771863544" alt="Whiskers change - moderately present" loading="lazy">', 71 + }, 72 + { 73 + score: 2, label: 'Straight and moving forward', sublabel: 'Markedly present', 74 + img: '<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-d-whiskers-change-2.jpg?t=1771863545" alt="Whiskers change - markedly present" loading="lazy">', 75 + }, 76 + ], 77 + }, 78 + { 79 + id: 'head', 80 + label: 'Head Position', 81 + options: [ 82 + { 83 + score: 0, label: 'Head above the shoulder line', sublabel: 'Absent', 84 + img: '<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-e-head-position-0.jpg?t=1771863545" alt="Head position - absent" loading="lazy">', 85 + }, 86 + { 87 + score: 1, label: 'Head aligned with the shoulder line', sublabel: 'Moderately present or uncertain', 88 + img: '<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-e-head-position-1.jpg?t=1771863546" alt="Head position - moderately present" loading="lazy">', 89 + }, 90 + { 91 + score: 2, label: 'Head below the shoulder line or tilted down', sublabel: 'Markedly present', 92 + img: '<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-e-head-position-2.jpg?t=1771863547" alt="Head position - markedly present" loading="lazy">', 93 + }, 94 + ], 95 + }, 96 + ]; 97 + 98 + const unitIds = ['ears', 'eyes', 'muzzle', 'whiskers', 'head']; 99 + 100 + // ── Helpers ─────────────────────────────────────────────────────────── 101 + 102 + function getColorClass(total) { 103 + if (total === 0) return 'color-0'; 104 + if (total <= 3) return 'color-mild'; 105 + if (total <= 8) return 'color-pain'; 106 + return 'color-severe'; 107 + } 108 + 109 + function getBadgeClass(total) { 110 + if (total === 0) return 'badge-0'; 111 + if (total <= 3) return 'badge-mild'; 112 + if (total <= 8) return 'badge-pain'; 113 + return 'badge-severe'; 114 + } 115 + 116 + function getBadgeLabel(total) { 117 + if (total === 0) return 'No Pain'; 118 + if (total <= 3) return 'Mild / No Pain'; 119 + if (total <= 8) return 'Likely In Pain'; 120 + return 'Severe Pain'; 121 + } 122 + 123 + function getInterpText(total) { 124 + if (total === 0) return 'This cat is not in pain. If you are a cat owner and are concerned, please consult your veterinary surgeon.'; 125 + 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.'; 126 + 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. Clinical judgement will differentiate if the FGS scores are high due to pain, rather than other factors such as stress, fear or sedation.'; 127 + 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. Clinical judgement will differentiate if the FGS scores are high due to pain, rather than other factors such as stress, fear or sedation.'; 128 + } 129 + 130 + // ── Page Class ──────────────────────────────────────────────────────── 131 + 132 + export default class FelineGrimaceScale extends PageManager { 133 + onReady() { 134 + this.scores = { ears: null, eyes: null, muzzle: null, whiskers: null, head: null }; 135 + this.buildUI(); 136 + this.updateScorePanel(); 137 + } 138 + 139 + getTotal() { 140 + return unitIds.reduce((sum, id) => sum + (this.scores[id] ?? 0), 0); 141 + } 142 + 143 + updateScorePanel() { 144 + const total = this.getTotal(); 145 + const colorClass = getColorClass(total); 146 + 147 + const totalEl = document.getElementById('total-display'); 148 + totalEl.textContent = total; 149 + totalEl.className = `total-number ${colorClass}`; 150 + totalEl.classList.add('pop-animate'); 151 + totalEl.addEventListener('animationend', () => totalEl.classList.remove('pop-animate'), { once: true }); 152 + 153 + const badge = document.getElementById('interp-badge'); 154 + badge.className = `interp-badge ${getBadgeClass(total)}`; 155 + badge.textContent = getBadgeLabel(total); 156 + 157 + document.getElementById('interp-text').textContent = getInterpText(total); 158 + 159 + const fill = document.getElementById('progress-fill'); 160 + fill.style.width = `${(total / 10) * 100}%`; 161 + fill.className = `progress-fill ${colorClass}`; 162 + 163 + unitIds.forEach(id => { 164 + const el = document.getElementById(`score-val-${id}`); 165 + if (el) { 166 + el.textContent = this.scores[id] !== null ? this.scores[id] : '—'; 167 + el.style.color = this.scores[id] !== null ? 'var(--fgs-green)' : 'var(--fgs-text-muted)'; 168 + } 169 + }); 170 + 171 + unitIds.forEach(id => { 172 + const unitBadge = document.getElementById(`unit-badge-${id}`); 173 + if (unitBadge) { 174 + if (this.scores[id] !== null) { 175 + unitBadge.textContent = `Score: ${this.scores[id]}`; 176 + unitBadge.classList.add('selected'); 177 + } else { 178 + unitBadge.textContent = 'Not scored'; 179 + unitBadge.classList.remove('selected'); 180 + } 181 + } 182 + }); 183 + } 184 + 185 + buildUI() { 186 + const container = document.getElementById('units-container'); 187 + const breakdown = document.getElementById('score-breakdown'); 188 + 189 + if (!container || !breakdown) return; 190 + 191 + const labels = ['Ears', 'Eyes', 'Muzzle', 'Whiskers', 'Head']; 192 + unitIds.forEach((id, i) => { 193 + const item = document.createElement('div'); 194 + item.className = 'score-item'; 195 + item.innerHTML = /* html */ `<div class="score-item-label">${labels[i]}</div><div class="score-item-value" id="score-val-${id}">—</div>`; 196 + breakdown.appendChild(item); 197 + }); 198 + 199 + units.forEach(unit => { 200 + const card = document.createElement('div'); 201 + card.className = 'unit-card'; 202 + card.innerHTML = /* html */ ` 203 + <div class="unit-header"> 204 + <h2>${unit.label}</h2> 205 + <span class="unit-score-badge" id="unit-badge-${unit.id}">Not scored</span> 206 + </div> 207 + <div class="options-row" role="group" aria-label="${unit.label}"> 208 + ${unit.options.map(opt => /* html */ ` 209 + <button class="option-btn" data-unit="${unit.id}" data-score="${opt.score}" aria-pressed="false"> 210 + <div class="score-pill">${opt.score}</div> 211 + <div class="cat-illustration">${opt.img}</div> 212 + <div class="option-label">${opt.label}</div> 213 + <div class="option-sublabel">${opt.sublabel}</div> 214 + </button> 215 + `).join('')} 216 + </div> 217 + `; 218 + container.appendChild(card); 219 + }); 220 + 221 + container.addEventListener('click', e => { 222 + const btn = e.target.closest('.option-btn'); 223 + if (!btn) return; 224 + const unitId = btn.dataset.unit; 225 + const score = parseInt(btn.dataset.score, 10); 226 + 227 + const siblings = btn.closest('.options-row').querySelectorAll('.option-btn'); 228 + siblings.forEach(s => { s.classList.remove('selected'); s.setAttribute('aria-pressed', 'false'); }); 229 + 230 + btn.classList.add('selected'); 231 + btn.setAttribute('aria-pressed', 'true'); 232 + this.scores[unitId] = score; 233 + 234 + this.updateScorePanel(); 235 + }); 236 + } 237 + }
+19
assets/scss/custom/_marketing-tokens.scss
··· 1 + // ── Shared Marketing Page Tokens ─────────────────────────────────────── 2 + // Used by Diet Builder, Feline Grimace Scale, and any future marketing/tool 3 + // pages that follow this design language. 4 + // 5 + // Individual page files alias these into their own namespace (--db-*, --fgs-*) 6 + // so they can override per-page without breaking others. 7 + 8 + :root { 9 + --brand-bg: #f7f4f0; 10 + --brand-card: #ffffff; 11 + --brand-green: #546052; 12 + --brand-green-light: #edf1ec; 13 + --brand-green-mid: #859e7d; 14 + --brand-text: #2a3329; 15 + --brand-text-muted: #687e6b; 16 + --brand-border: #d4ddd3; 17 + --brand-radius: 16px; 18 + --brand-transition: 0.22s cubic-bezier(0.4, 0, 0.2, 1); 19 + }
+2
assets/scss/custom/main.scss
··· 1 1 @import 'variables'; 2 + @import 'marketing-tokens'; 2 3 /* Custom Page Styling */ 3 4 @import 'pages/cart'; 4 5 @import 'pages/pdp'; 5 6 @import 'pages/calculators'; 6 7 @import 'pages/diet-builder'; 8 + @import 'pages/feline-grimace-scale'; 7 9 @import 'pages/account'; 8 10 9 11 /*Components*/
+12 -12
assets/scss/custom/pages/_diet-builder.scss
··· 1 1 // ── Diet Builder ───────────────────────────────────────────────────── 2 2 3 - // CSS Variables (matching feline-grimace-scale) 3 + // CSS Variables — aliased from shared marketing tokens (_marketing-tokens.scss) 4 4 :root { 5 - --db-bg: #f7f4f0; 6 - --db-card: #ffffff; 7 - --db-green: #546052; 8 - --db-green-light: #edf1ec; 9 - --db-green-mid: #859e7d; 10 - --db-text: #2a3329; 11 - --db-text-muted: #687e6b; 12 - --db-selected: #546052; 13 - --db-border: #d4ddd3; 14 - --db-radius: 16px; 15 - --db-transition: 0.22s cubic-bezier(0.4, 0, 0.2, 1); 5 + --db-bg: var(--brand-bg); 6 + --db-card: var(--brand-card); 7 + --db-green: var(--brand-green); 8 + --db-green-light: var(--brand-green-light); 9 + --db-green-mid: var(--brand-green-mid); 10 + --db-text: var(--brand-text); 11 + --db-text-muted: var(--brand-text-muted); 12 + --db-selected: var(--brand-green); 13 + --db-border: var(--brand-border); 14 + --db-radius: var(--brand-radius); 15 + --db-transition: var(--brand-transition); 16 16 } 17 17 18 18 // Import fonts
+613
assets/scss/custom/pages/_feline-grimace-scale.scss
··· 1 + // ── Feline Grimace Scale ────────────────────────────────────────────── 2 + 3 + // CSS Variables — aliased from shared marketing tokens (_marketing-tokens.scss) 4 + // FGS-specific score colours are defined here only. 5 + :root { 6 + --fgs-bg: var(--brand-bg); 7 + --fgs-card: var(--brand-card); 8 + --fgs-green: var(--brand-green); 9 + --fgs-green-light: var(--brand-green-light); 10 + --fgs-green-mid: var(--brand-green-mid); 11 + --fgs-text: var(--brand-text); 12 + --fgs-text-muted: var(--brand-text-muted); 13 + --fgs-border: var(--brand-border); 14 + --fgs-score-low: #4caf87; 15 + --fgs-score-mid: #e8a020; 16 + --fgs-score-high: #e05555; 17 + --fgs-radius: var(--brand-radius); 18 + --fgs-transition: var(--brand-transition); 19 + } 20 + 21 + .fgs-page { 22 + width: 100vw; 23 + margin-left: calc(-50vw + 50%); 24 + background: var(--fgs-bg); 25 + min-height: 100vh; 26 + position: relative; 27 + 28 + &::before { 29 + content: ''; 30 + position: fixed; 31 + top: -120px; 32 + right: -120px; 33 + width: 420px; 34 + height: 420px; 35 + background: radial-gradient(circle, #859e7d30 0%, transparent 70%); 36 + pointer-events: none; 37 + z-index: 0; 38 + } 39 + 40 + &::after { 41 + content: ''; 42 + position: fixed; 43 + bottom: -80px; 44 + left: -80px; 45 + width: 320px; 46 + height: 320px; 47 + background: radial-gradient(circle, #687e6b20 0%, transparent 70%); 48 + pointer-events: none; 49 + z-index: 0; 50 + } 51 + } 52 + 53 + .fgs-wrapper { 54 + max-width: 820px; 55 + margin: 0 auto; 56 + padding: 40px 16px; 57 + position: relative; 58 + z-index: 1; 59 + } 60 + 61 + // ── Header ──────────────────────────────────────────────────────────── 62 + 63 + .fgs-header { 64 + text-align: center; 65 + margin-bottom: 40px; 66 + animation: fgsFadeDown 0.6s ease both; 67 + 68 + .fgs-paw-icon { 69 + font-size: 2.4rem; 70 + margin-bottom: 8px; 71 + display: block; 72 + } 73 + 74 + h1 { 75 + font-size: clamp(1.8rem, 5vw, 2.8rem); 76 + font-weight: 700; 77 + color: var(--fgs-green); 78 + line-height: 1.1; 79 + letter-spacing: -0.5px; 80 + } 81 + 82 + p { 83 + margin-top: 10px; 84 + color: var(--fgs-text-muted); 85 + font-size: 0.95rem; 86 + font-weight: 300; 87 + max-width: 480px; 88 + margin-inline: auto; 89 + line-height: 1.6; 90 + } 91 + } 92 + 93 + // ── Notice ──────────────────────────────────────────────────────────── 94 + 95 + .fgs-notice { 96 + background: #fff8e6; 97 + border: 1.5px solid #f5c842; 98 + border-radius: 10px; 99 + padding: 12px 18px; 100 + font-size: 0.82rem; 101 + color: #7a5c00; 102 + text-align: center; 103 + margin-bottom: 32px; 104 + line-height: 1.5; 105 + animation: fgsFadeDown 0.7s ease both; 106 + } 107 + 108 + // ── Instructions ────────────────────────────────────────────────────── 109 + 110 + .fgs-instructions { 111 + background: var(--fgs-green-light); 112 + border-radius: var(--fgs-radius); 113 + padding: 18px 22px; 114 + margin-bottom: 32px; 115 + font-size: 0.88rem; 116 + color: var(--fgs-text); 117 + line-height: 1.7; 118 + animation: fgsFadeDown 0.75s ease both; 119 + 120 + strong { 121 + color: var(--fgs-green); 122 + } 123 + 124 + .fgs-wait-list { 125 + display: flex; 126 + gap: 16px; 127 + flex-wrap: wrap; 128 + margin-top: 8px; 129 + 130 + span { 131 + background: white; 132 + border-radius: 20px; 133 + padding: 4px 14px; 134 + font-size: 0.82rem; 135 + font-weight: 500; 136 + color: var(--fgs-green); 137 + border: 1.5px solid var(--fgs-green-mid); 138 + } 139 + } 140 + } 141 + 142 + // ── Action Unit Cards ───────────────────────────────────────────────── 143 + 144 + .unit-card { 145 + background: var(--fgs-card); 146 + border-radius: var(--fgs-radius); 147 + border: 1.5px solid var(--fgs-border); 148 + margin-bottom: 24px; 149 + overflow: hidden; 150 + animation: fgsFadeUp 0.5s ease both; 151 + box-shadow: 0 2px 20px #54605208; 152 + transition: box-shadow var(--fgs-transition); 153 + 154 + &:hover { 155 + box-shadow: 0 6px 32px #54605214; 156 + } 157 + 158 + &:nth-child(1) { 159 + animation-delay: 0.1s; 160 + } 161 + &:nth-child(2) { 162 + animation-delay: 0.15s; 163 + } 164 + &:nth-child(3) { 165 + animation-delay: 0.2s; 166 + } 167 + &:nth-child(4) { 168 + animation-delay: 0.25s; 169 + } 170 + &:nth-child(5) { 171 + animation-delay: 0.3s; 172 + } 173 + } 174 + 175 + .unit-header { 176 + background: var(--fgs-green); 177 + color: white; 178 + padding: 14px 22px; 179 + display: flex; 180 + align-items: center; 181 + gap: 10px; 182 + 183 + h2 { 184 + color: white; 185 + font-size: 0.82rem; 186 + font-weight: 600; 187 + letter-spacing: 2px; 188 + text-transform: uppercase; 189 + } 190 + 191 + .unit-score-badge { 192 + margin-left: auto; 193 + background: rgba(255, 255, 255, 0.2); 194 + border-radius: 20px; 195 + padding: 4px 14px; 196 + font-size: 0.82rem; 197 + font-weight: 600; 198 + transition: background var(--fgs-transition); 199 + 200 + &.selected { 201 + background: white; 202 + color: var(--fgs-green); 203 + } 204 + } 205 + } 206 + 207 + .options-row { 208 + display: grid; 209 + grid-template-columns: repeat(3, 1fr); 210 + gap: 0; 211 + border-top: 1px solid var(--fgs-border); 212 + } 213 + 214 + .option-btn { 215 + position: relative; 216 + display: flex; 217 + flex-direction: column; 218 + align-items: center; 219 + padding: 24px 16px 20px; 220 + cursor: pointer; 221 + border: none; 222 + background: transparent; 223 + transition: background var(--fgs-transition); 224 + border-right: 1px solid var(--fgs-border); 225 + outline: none; 226 + text-align: center; 227 + -webkit-tap-highlight-color: transparent; 228 + 229 + &:last-child { 230 + border-right: none; 231 + } 232 + 233 + &:hover { 234 + background: var(--fgs-green-light); 235 + } 236 + 237 + &.selected { 238 + background: var(--fgs-green-light); 239 + 240 + &::after { 241 + content: ''; 242 + position: absolute; 243 + inset: 0; 244 + border: 2.5px solid var(--fgs-green); 245 + border-radius: 0; 246 + pointer-events: none; 247 + } 248 + 249 + .score-pill { 250 + border-color: var(--fgs-green); 251 + background: var(--fgs-green); 252 + color: white; 253 + transform: scale(1.1); 254 + } 255 + } 256 + } 257 + 258 + .score-pill { 259 + width: 32px; 260 + height: 32px; 261 + border-radius: 50%; 262 + border: 2.5px solid var(--fgs-border); 263 + display: flex; 264 + align-items: center; 265 + justify-content: center; 266 + font-weight: 700; 267 + font-size: 0.9rem; 268 + color: var(--fgs-text-muted); 269 + margin-bottom: 14px; 270 + transition: all var(--fgs-transition); 271 + background: white; 272 + } 273 + 274 + .cat-illustration { 275 + width: 110px; 276 + height: 90px; 277 + margin-bottom: 12px; 278 + 279 + img { 280 + width: 100%; 281 + height: 100%; 282 + object-fit: contain; 283 + display: block; 284 + mix-blend-mode: multiply; 285 + } 286 + } 287 + 288 + .option-label { 289 + font-size: 0.8rem; 290 + font-weight: 600; 291 + color: var(--fgs-text); 292 + line-height: 1.3; 293 + margin-bottom: 4px; 294 + } 295 + 296 + .option-sublabel { 297 + font-size: 0.72rem; 298 + color: var(--fgs-text-muted); 299 + line-height: 1.3; 300 + font-weight: 400; 301 + } 302 + 303 + // ── Score Panel ─────────────────────────────────────────────────────── 304 + 305 + .score-panel { 306 + background: var(--fgs-card); 307 + border-radius: var(--fgs-radius); 308 + border: 1.5px solid var(--fgs-border); 309 + margin-bottom: 24px; 310 + overflow: hidden; 311 + animation: fgsFadeUp 0.5s 0.4s ease both; 312 + box-shadow: 0 2px 20px #54605208; 313 + } 314 + 315 + .score-panel-header { 316 + background: var(--fgs-text); 317 + color: white; 318 + padding: 16px 22px; 319 + display: flex; 320 + align-items: center; 321 + gap: 10px; 322 + 323 + h2 { 324 + color: white; 325 + font-size: 1.1rem; 326 + font-weight: 600; 327 + } 328 + } 329 + 330 + .score-breakdown { 331 + display: flex; 332 + padding: 20px 22px 0; 333 + gap: 0; 334 + flex-wrap: wrap; 335 + } 336 + 337 + .score-item { 338 + flex: 1; 339 + min-width: 100px; 340 + text-align: center; 341 + padding: 12px 8px; 342 + border-right: 1px solid var(--fgs-border); 343 + 344 + &:last-child { 345 + border-right: none; 346 + } 347 + } 348 + 349 + .score-item-label { 350 + font-size: 0.7rem; 351 + font-weight: 600; 352 + letter-spacing: 1.5px; 353 + text-transform: uppercase; 354 + color: var(--fgs-text-muted); 355 + margin-bottom: 6px; 356 + } 357 + 358 + .score-item-value { 359 + font-size: 1.6rem; 360 + font-weight: 700; 361 + color: var(--fgs-text); 362 + transition: all var(--fgs-transition); 363 + } 364 + 365 + .total-section { 366 + padding: 20px 22px; 367 + display: flex; 368 + align-items: center; 369 + gap: 24px; 370 + border-top: 1.5px solid var(--fgs-border); 371 + margin-top: 16px; 372 + flex-wrap: wrap; 373 + } 374 + 375 + .total-number { 376 + font-size: 3.6rem; 377 + margin-right: 3px; 378 + font-weight: 700; 379 + line-height: 1; 380 + min-width: 80px; 381 + text-align: center; 382 + transition: color 0.4s ease; 383 + 384 + &.color-0 { 385 + color: var(--fgs-score-low); 386 + } 387 + &.color-mild { 388 + color: var(--fgs-score-low); 389 + } 390 + &.color-pain { 391 + color: var(--fgs-score-mid); 392 + } 393 + &.color-severe { 394 + color: var(--fgs-score-high); 395 + } 396 + } 397 + 398 + .total-label { 399 + font-size: 0.72rem; 400 + font-weight: 600; 401 + letter-spacing: 1.5px; 402 + text-transform: uppercase; 403 + color: var(--fgs-text-muted); 404 + margin-bottom: 4px; 405 + } 406 + 407 + .total-row { 408 + display: flex; 409 + align-items: flex-end; 410 + gap: 4px; 411 + } 412 + 413 + .total-out-of { 414 + font-size: 1rem; 415 + color: var(--fgs-text-muted); 416 + font-weight: 300; 417 + align-self: flex-end; 418 + padding-bottom: 8px; 419 + margin-left: -16px; 420 + } 421 + 422 + .interpretation { 423 + flex: 1; 424 + min-width: 200px; 425 + } 426 + 427 + .interp-badge { 428 + display: inline-block; 429 + border-radius: 20px; 430 + padding: 5px 16px; 431 + font-size: 0.75rem; 432 + font-weight: 700; 433 + letter-spacing: 1px; 434 + text-transform: uppercase; 435 + margin-bottom: 8px; 436 + transition: all 0.4s ease; 437 + 438 + &.badge-0, 439 + &.badge-mild { 440 + background: #e8f5ef; 441 + color: #2d7a59; 442 + } 443 + &.badge-pain { 444 + background: #fff3e0; 445 + color: #b56b00; 446 + } 447 + &.badge-severe { 448 + background: #fde8e8; 449 + color: #c0392b; 450 + } 451 + } 452 + 453 + .interp-text { 454 + font-size: 0.85rem; 455 + line-height: 1.6; 456 + color: var(--fgs-text); 457 + transition: opacity 0.3s ease; 458 + } 459 + 460 + // ── Progress Bar ────────────────────────────────────────────────────── 461 + 462 + .progress-bar-wrap { 463 + padding: 0 22px 22px; 464 + } 465 + 466 + .progress-label { 467 + display: flex; 468 + justify-content: space-between; 469 + font-size: 0.75rem; 470 + color: var(--fgs-text-muted); 471 + margin-bottom: 6px; 472 + font-weight: 500; 473 + } 474 + 475 + .progress-track { 476 + height: 8px; 477 + background: var(--fgs-border); 478 + border-radius: 20px; 479 + overflow: hidden; 480 + } 481 + 482 + .progress-fill { 483 + height: 100%; 484 + border-radius: 20px; 485 + width: 0%; 486 + transition: 487 + width 0.5s cubic-bezier(0.4, 0, 0.2, 1), 488 + background 0.4s ease; 489 + background: var(--fgs-score-low); 490 + 491 + &.color-pain { 492 + background: var(--fgs-score-mid); 493 + } 494 + &.color-severe { 495 + background: var(--fgs-score-high); 496 + } 497 + } 498 + 499 + .cutoff-indicator { 500 + position: relative; 501 + height: 16px; 502 + margin-top: 4px; 503 + } 504 + 505 + .cutoff-line { 506 + position: absolute; 507 + left: 40%; 508 + top: 0; 509 + width: 2px; 510 + height: 14px; 511 + background: var(--fgs-score-mid); 512 + border-radius: 2px; 513 + } 514 + 515 + .cutoff-label { 516 + position: absolute; 517 + left: calc(40% + 5px); 518 + transform: translateX(-50%); 519 + top: 0; 520 + font-size: 0.65rem; 521 + color: var(--fgs-score-mid); 522 + font-weight: 600; 523 + white-space: nowrap; 524 + padding-left: 6px; 525 + } 526 + 527 + .fgs-attribution { 528 + font-size: 0.65rem; 529 + color: var(--fgs-text-muted); 530 + text-align: right; 531 + padding: 0 22px 12px; 532 + font-style: italic; 533 + } 534 + 535 + // ── Animations ──────────────────────────────────────────────────────── 536 + 537 + @keyframes fgsFadeDown { 538 + from { 539 + opacity: 0; 540 + transform: translateY(-16px); 541 + } 542 + to { 543 + opacity: 1; 544 + transform: translateY(0); 545 + } 546 + } 547 + 548 + @keyframes fgsFadeUp { 549 + from { 550 + opacity: 0; 551 + transform: translateY(20px); 552 + } 553 + to { 554 + opacity: 1; 555 + transform: translateY(0); 556 + } 557 + } 558 + 559 + @keyframes fgsPop { 560 + 0% { 561 + transform: scale(1); 562 + } 563 + 50% { 564 + transform: scale(1.15); 565 + } 566 + 100% { 567 + transform: scale(1); 568 + } 569 + } 570 + 571 + .pop-animate { 572 + animation: fgsPop 0.3s ease; 573 + } 574 + 575 + // ── Responsive ──────────────────────────────────────────────────────── 576 + 577 + @media (max-width: 600px) { 578 + .options-row { 579 + grid-template-columns: 1fr; 580 + } 581 + 582 + .option-btn { 583 + border-right: none; 584 + border-bottom: 1px solid var(--fgs-border); 585 + flex-direction: row; 586 + gap: 14px; 587 + text-align: left; 588 + padding: 16px; 589 + 590 + &:last-child { 591 + border-bottom: none; 592 + } 593 + } 594 + 595 + .cat-illustration { 596 + width: 70px; 597 + height: 60px; 598 + margin-bottom: 0; 599 + flex-shrink: 0; 600 + } 601 + 602 + .score-breakdown { 603 + gap: 0; 604 + } 605 + 606 + .score-item { 607 + min-width: 60px; 608 + } 609 + 610 + .total-section { 611 + gap: 14px; 612 + } 613 + }
+52 -774
templates/pages/custom/page/feline-grimace-scale.html
··· 1 1 {{#partial "page"}} 2 - <style> 3 - :root { 4 - --fgs-bg: #f7f4f0; 5 - --fgs-card: #ffffff; 6 - --fgs-green: #546052; 7 - --fgs-green-light: #edf1ec; 8 - --fgs-green-mid: #859E7D; 9 - --fgs-text: #2a3329; 10 - --fgs-text-muted: #687E6B; 11 - --fgs-border: #d4ddd3; 12 - --fgs-score-low: #4caf87; 13 - --fgs-score-mid: #e8a020; 14 - --fgs-score-high: #e05555; 15 - --fgs-radius: 16px; 16 - --fgs-transition: 0.22s cubic-bezier(0.4,0,0.2,1); 17 - } 18 - 19 - *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 20 - 21 - body { 22 - background: var(--fgs-bg); 23 - color: var(--fgs-text); 24 - min-height: 100vh; 25 - } 26 - 27 - body::before { 28 - content: ''; 29 - position: fixed; 30 - top: -120px; right: -120px; 31 - width: 420px; height: 420px; 32 - background: radial-gradient(circle, #859e7d30 0%, transparent 70%); 33 - pointer-events: none; 34 - z-index: 0; 35 - } 36 - 37 - body::after { 38 - content: ''; 39 - position: fixed; 40 - bottom: -80px; left: -80px; 41 - width: 320px; height: 320px; 42 - background: radial-gradient(circle, #687e6b20 0%, transparent 70%); 43 - pointer-events: none; 44 - z-index: 0; 45 - } 46 - 47 - .wrapper { 48 - max-width: 820px; 49 - margin: 0 auto; 50 - position: relative; 51 - z-index: 1; 52 - } 53 - 54 - header { 55 - text-align: center; 56 - margin-bottom: 40px; 57 - animation: fadeDown 0.6s ease both; 58 - } 59 - 60 - .paw-icon { 61 - font-size: 2.4rem; 62 - margin-bottom: 8px; 63 - display: block; 64 - } 65 - 66 - header h1 { 67 - font-size: clamp(1.8rem, 5vw, 2.8rem); 68 - font-weight: 700; 69 - color: var(--fgs-green); 70 - line-height: 1.1; 71 - letter-spacing: -0.5px; 72 - } 73 - 74 - header p { 75 - margin-top: 10px; 76 - color: var(--fgs-text-muted); 77 - font-size: 0.95rem; 78 - font-weight: 300; 79 - max-width: 480px; 80 - margin-inline: auto; 81 - line-height: 1.6; 82 - } 83 - 84 - .notice { 85 - background: #fff8e6; 86 - border: 1.5px solid #f5c842; 87 - border-radius: 10px; 88 - padding: 12px 18px; 89 - font-size: 0.82rem; 90 - color: #7a5c00; 91 - text-align: center; 92 - margin-bottom: 32px; 93 - line-height: 1.5; 94 - animation: fadeDown 0.7s ease both; 95 - } 96 - 97 - .instructions { 98 - background: var(--fgs-green-light); 99 - border-radius: var(--fgs-radius); 100 - padding: 18px 22px; 101 - margin-bottom: 32px; 102 - font-size: 0.88rem; 103 - color: var(--fgs-text); 104 - line-height: 1.7; 105 - animation: fadeDown 0.75s ease both; 106 - } 107 - 108 - .instructions strong { color: var(--fgs-green); } 109 - 110 - .instructions .wait-list { 111 - display: flex; 112 - gap: 16px; 113 - flex-wrap: wrap; 114 - margin-top: 8px; 115 - } 116 - 117 - .wait-list span { 118 - background: white; 119 - border-radius: 20px; 120 - padding: 4px 14px; 121 - font-size: 0.82rem; 122 - font-weight: 500; 123 - color: var(--fgs-green); 124 - border: 1.5px solid var(--fgs-green-mid); 125 - } 126 - 127 - /* Action Unit Cards */ 128 - .unit-card { 129 - background: var(--fgs-card); 130 - border-radius: var(--fgs-radius); 131 - border: 1.5px solid var(--fgs-border); 132 - margin-bottom: 24px; 133 - overflow: hidden; 134 - animation: fadeUp 0.5s ease both; 135 - box-shadow: 0 2px 20px #54605208; 136 - transition: box-shadow var(--fgs-transition); 137 - } 138 - 139 - .unit-card:hover { box-shadow: 0 6px 32px #54605214; } 140 - .unit-card:nth-child(1) { animation-delay: 0.1s; } 141 - .unit-card:nth-child(2) { animation-delay: 0.15s; } 142 - .unit-card:nth-child(3) { animation-delay: 0.2s; } 143 - .unit-card:nth-child(4) { animation-delay: 0.25s; } 144 - .unit-card:nth-child(5) { animation-delay: 0.3s; } 145 - 146 - .unit-header { 147 - background: var(--fgs-green); 148 - color: white; 149 - padding: 14px 22px; 150 - display: flex; 151 - align-items: center; 152 - gap: 10px; 153 - } 154 - 155 - .unit-header h2 { 156 - color: white; 157 - font-size: 0.82rem; 158 - font-weight: 600; 159 - letter-spacing: 2px; 160 - text-transform: uppercase; 161 - } 162 - 163 - .unit-header .unit-score-badge { 164 - margin-left: auto; 165 - background: rgba(255,255,255,0.2); 166 - border-radius: 20px; 167 - padding: 4px 14px; 168 - font-size: 0.82rem; 169 - font-weight: 600; 170 - transition: background var(--fgs-transition); 171 - } 172 - 173 - .unit-header .unit-score-badge.selected { 174 - background: white; 175 - color: var(--fgs-green); 176 - } 177 - 178 - .options-row { 179 - display: grid; 180 - grid-template-columns: repeat(3, 1fr); 181 - gap: 0; 182 - border-top: 1px solid var(--fgs-border); 183 - } 184 - 185 - .option-btn { 186 - position: relative; 187 - display: flex; 188 - flex-direction: column; 189 - align-items: center; 190 - padding: 24px 16px 20px; 191 - cursor: pointer; 192 - border: none; 193 - background: transparent; 194 - transition: background var(--fgs-transition); 195 - border-right: 1px solid var(--fgs-border); 196 - outline: none; 197 - text-align: center; 198 - -webkit-tap-highlight-color: transparent; 199 - } 200 - 201 - .option-btn:last-child { border-right: none; } 202 - 203 - .option-btn:hover { background: var(--fgs-green-light); } 204 - 205 - .option-btn.selected { 206 - background: var(--fgs-green-light); 207 - } 208 - 209 - .option-btn.selected::after { 210 - content: ''; 211 - position: absolute; 212 - inset: 0; 213 - border: 2.5px solid var(--fgs-green); 214 - border-radius: 0; 215 - pointer-events: none; 216 - } 217 - 218 - .option-btn:first-child.selected::after { border-radius: 0; } 219 - 220 - .score-pill { 221 - width: 32px; height: 32px; 222 - border-radius: 50%; 223 - border: 2.5px solid var(--fgs-border); 224 - display: flex; align-items: center; justify-content: center; 225 - font-weight: 700; 226 - font-size: 0.9rem; 227 - color: var(--fgs-text-muted); 228 - margin-bottom: 14px; 229 - transition: all var(--fgs-transition); 230 - background: white; 231 - } 232 - 233 - .option-btn.selected .score-pill { 234 - border-color: var(--fgs-green); 235 - background: var(--fgs-green); 236 - color: white; 237 - transform: scale(1.1); 238 - } 239 - 240 - .cat-illustration { 241 - width: 110px; 242 - height: 90px; 243 - margin-bottom: 12px; 244 - } 245 - 246 - .cat-illustration img { 247 - width: 100%; 248 - height: 100%; 249 - object-fit: contain; 250 - display: block; 251 - mix-blend-mode: multiply; 252 - } 253 - 254 - .option-label { 255 - font-size: 0.8rem; 256 - font-weight: 600; 257 - color: var(--fgs-text); 258 - line-height: 1.3; 259 - margin-bottom: 4px; 260 - } 261 - 262 - .option-sublabel { 263 - font-size: 0.72rem; 264 - color: var(--fgs-text-muted); 265 - line-height: 1.3; 266 - font-weight: 400; 267 - } 268 - 269 - /* Score Panel */ 270 - .score-panel { 271 - background: var(--fgs-card); 272 - border-radius: var(--fgs-radius); 273 - border: 1.5px solid var(--fgs-border); 274 - margin-bottom: 24px; 275 - overflow: hidden; 276 - animation: fadeUp 0.5s 0.4s ease both; 277 - box-shadow: 0 2px 20px #54605208; 278 - } 279 - 280 - .score-panel-header { 281 - background: var(--fgs-text); 282 - color: white; 283 - padding: 16px 22px; 284 - display: flex; 285 - align-items: center; 286 - gap: 10px; 287 - } 288 - 289 - .score-panel-header h2 { 290 - color: white; 291 - font-size: 1.1rem; 292 - font-weight: 600; 293 - } 294 - 295 - .score-breakdown { 296 - display: flex; 297 - padding: 20px 22px 0; 298 - gap: 0; 299 - flex-wrap: wrap; 300 - } 301 - 302 - .score-item { 303 - flex: 1; 304 - min-width: 100px; 305 - text-align: center; 306 - padding: 12px 8px; 307 - border-right: 1px solid var(--fgs-border); 308 - } 309 - 310 - .score-item:last-child { border-right: none; } 311 - 312 - .score-item-label { 313 - font-size: 0.7rem; 314 - font-weight: 600; 315 - letter-spacing: 1.5px; 316 - text-transform: uppercase; 317 - color: var(--fgs-text-muted); 318 - margin-bottom: 6px; 319 - } 320 - 321 - .score-item-value { 322 - font-size: 1.6rem; 323 - font-weight: 700; 324 - color: var(--fgs-text); 325 - transition: all var(--fgs-transition); 326 - } 327 - 328 - .total-section { 329 - padding: 20px 22px; 330 - display: flex; 331 - align-items: center; 332 - gap: 24px; 333 - border-top: 1.5px solid var(--fgs-border); 334 - margin-top: 16px; 335 - flex-wrap: wrap; 336 - } 337 - 338 - .total-number { 339 - font-size: 3.6rem; 340 - margin-right: 3px; 341 - font-weight: 700; 342 - line-height: 1; 343 - min-width: 80px; 344 - text-align: center; 345 - transition: color 0.4s ease; 346 - } 347 - 348 - .total-number.color-0 { color: var(--fgs-score-low); } 349 - .total-number.color-mild { color: var(--fgs-score-low); } 350 - .total-number.color-pain { color: var(--fgs-score-mid); } 351 - .total-number.color-severe { color: var(--fgs-score-high); } 352 - 353 - .total-out-of { 354 - font-size: 1rem; 355 - color: var(--fgs-text-muted); 356 - font-weight: 300; 357 - align-self: flex-end; 358 - padding-bottom: 8px; 359 - margin-left: -16px; 360 - } 361 - 362 - .interpretation { 363 - flex: 1; 364 - min-width: 200px; 365 - } 366 - 367 - .interp-badge { 368 - display: inline-block; 369 - border-radius: 20px; 370 - padding: 5px 16px; 371 - font-size: 0.75rem; 372 - font-weight: 700; 373 - letter-spacing: 1px; 374 - text-transform: uppercase; 375 - margin-bottom: 8px; 376 - transition: all 0.4s ease; 377 - } 378 - 379 - .badge-0, .badge-mild { background: #e8f5ef; color: #2d7a59; } 380 - .badge-pain { background: #fff3e0; color: #b56b00; } 381 - .badge-severe { background: #fde8e8; color: #c0392b; } 382 - 383 - .interp-text { 384 - font-size: 0.85rem; 385 - line-height: 1.6; 386 - color: var(--fgs-text); 387 - transition: opacity 0.3s ease; 388 - } 389 - 390 - .progress-bar-wrap { 391 - padding: 0 22px 22px; 392 - } 393 - 394 - .progress-label { 395 - display: flex; 396 - justify-content: space-between; 397 - font-size: 0.75rem; 398 - color: var(--fgs-text-muted); 399 - margin-bottom: 6px; 400 - font-weight: 500; 401 - } 402 - 403 - .progress-track { 404 - height: 8px; 405 - background: var(--fgs-border); 406 - border-radius: 20px; 407 - overflow: hidden; 408 - } 409 - 410 - .progress-fill { 411 - height: 100%; 412 - border-radius: 20px; 413 - width: 0%; 414 - transition: width 0.5s cubic-bezier(0.4,0,0.2,1), background 0.4s ease; 415 - background: var(--fgs-score-low); 416 - } 417 - 418 - .progress-fill.color-pain { background: var(--fgs-score-mid); } 419 - .progress-fill.color-severe { background: var(--fgs-score-high); } 420 - 421 - .cutoff-indicator { 422 - position: relative; 423 - height: 16px; 424 - margin-top: 4px; 425 - } 426 - 427 - .cutoff-line { 428 - position: absolute; 429 - left: 40%; 430 - top: 0; 431 - width: 2px; 432 - height: 14px; 433 - background: var(--fgs-score-mid); 434 - border-radius: 2px; 435 - } 436 - 437 - .cutoff-label { 438 - position: absolute; 439 - left: calc(40% + 5px); 440 - transform: translateX(-50%); 441 - top: 0; 442 - font-size: 0.65rem; 443 - color: var(--fgs-score-mid); 444 - font-weight: 600; 445 - white-space: nowrap; 446 - padding-left: 6px; 447 - } 448 - 449 - .attribution { 450 - font-size: 0.65rem; 451 - color: var(--fgs-text-muted); 452 - text-align: right; 453 - padding: 0 22px 12px; 454 - font-style: italic; 455 - } 456 - 457 - /* Animations */ 458 - @keyframes fadeDown { 459 - from { opacity: 0; transform: translateY(-16px); } 460 - to { opacity: 1; transform: translateY(0); } 461 - } 462 - 463 - @keyframes fadeUp { 464 - from { opacity: 0; transform: translateY(20px); } 465 - to { opacity: 1; transform: translateY(0); } 466 - } 467 - 468 - @keyframes pop { 469 - 0% { transform: scale(1); } 470 - 50% { transform: scale(1.15); } 471 - 100% { transform: scale(1); } 472 - } 473 - 474 - .pop-animate { animation: pop 0.3s ease; } 475 - 476 - /* SVG cat illustrations */ 477 - svg { display: block; } 478 - 479 - @media (max-width: 600px) { 480 - .options-row { grid-template-columns: 1fr; } 481 - .option-btn { border-right: none; border-bottom: 1px solid var(--fgs-border); flex-direction: row; gap: 14px; text-align: left; padding: 16px; } 482 - .option-btn:last-child { border-bottom: none; } 483 - .cat-illustration { width: 70px; height: 60px; margin-bottom: 0; flex-shrink: 0; } 484 - .score-breakdown { gap: 0; } 485 - .score-item { min-width: 60px; } 486 - .total-section { gap: 14px; } 487 - } 488 - </style> 489 - <body> 490 - <div class="wrapper"> 2 + <div class="fgs-page"> 3 + <div class="fgs-wrapper"> 491 4 492 - <header> 493 - <span class="paw-icon">🐾</span> 494 - <h1>Feline Grimace Scale</h1> 495 - <p>Observe the cat undisturbed from a distance for 30 seconds, then score each action unit below.</p> 496 - </header> 5 + <header class="fgs-header"> 6 + <span class="fgs-paw-icon">🐾</span> 7 + <h1>Feline Grimace Scale</h1> 8 + <p>Observe the cat undisturbed from a distance for 30 seconds, then score each action unit below.</p> 9 + </header> 497 10 498 - <div class="notice"> 499 - ⚠️ <strong>For owners:</strong> Always consult your vet before administering any medication. Some human pain killers can be fatal to cats. The FGS has been developed and validated for acute pain assessment. If the cat has a chronic condition, the FGS is not reliable. 500 - </div> 11 + <div class="fgs-notice"> 12 + ⚠️ <strong>For owners:</strong> Always consult your vet before administering any medication. Some human pain killers can be fatal to cats. The FGS has been developed and validated for acute pain assessment. If the cat has a chronic condition, the FGS is not reliable. 13 + </div> 501 14 502 - <div class="instructions"> 503 - <strong>Observe the cat in real-time.</strong> Observe the cat awake and undisturbed from a distance for 30 seconds and then 504 - score each FGS action unit. If the cat is doing any of the following, wait until they have finished before scoring. 505 - <div class="wait-list"> 506 - <span>🧹 Grooming</span> 507 - <span>😴 Sleeping</span> 508 - <span>🎾 Playing</span> 509 - <span>🍽️ Eating</span> 15 + <div class="fgs-instructions"> 16 + <strong>Observe the cat in real-time.</strong> Observe the cat awake and undisturbed from a distance for 30 seconds and then 17 + score each FGS action unit. If the cat is doing any of the following, wait until they have finished before scoring. 18 + <div class="fgs-wait-list"> 19 + <span>🧹 Grooming</span> 20 + <span>😴 Sleeping</span> 21 + <span>🎾 Playing</span> 22 + <span>🍽️ Eating</span> 23 + </div> 510 24 </div> 511 - </div> 512 25 513 - <div id="units-container"></div> 26 + <div id="units-container"></div> 514 27 515 - <div class="score-panel"> 516 - <div class="score-panel-header"> 517 - <h2>Your Scores</h2> 518 - </div> 519 - <div class="score-breakdown" id="score-breakdown"></div> 520 - <div class="total-section"> 521 - <div> 522 - <div style="font-size:0.72rem;font-weight:600;letter-spacing:1.5px;text-transform:uppercase;color:var(--fgs-text-muted);margin-bottom:4px;">Total</div> 523 - <div style="display:flex;align-items:flex-end;gap:4px;"> 524 - <div class="total-number color-0" id="total-display">0</div> 525 - <div class="total-out-of">/ 10</div> 28 + <div class="score-panel"> 29 + <div class="score-panel-header"> 30 + <h2>Your Scores</h2> 31 + </div> 32 + <div class="score-breakdown" id="score-breakdown"></div> 33 + <div class="total-section"> 34 + <div> 35 + <div class="total-label">Total</div> 36 + <div class="total-row"> 37 + <div class="total-number color-0" id="total-display">0</div> 38 + <div class="total-out-of">/ 10</div> 39 + </div> 40 + </div> 41 + <div class="interpretation"> 42 + <div class="interp-badge badge-0" id="interp-badge">No Pain</div> 43 + <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 44 </div> 527 45 </div> 528 - <div class="interpretation"> 529 - <div class="interp-badge badge-0" id="interp-badge">No Pain</div> 530 - <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> 46 + <div class="progress-bar-wrap"> 47 + <div class="progress-label"> 48 + <span>0</span> 49 + <span>Rescue analgesia threshold: 4</span> 50 + <span>10</span> 51 + </div> 52 + <div class="progress-track"> 53 + <div class="progress-fill" id="progress-fill"></div> 54 + </div> 55 + <div class="cutoff-indicator"> 56 + <div class="cutoff-line"></div> 57 + <span class="cutoff-label">4</span> 58 + </div> 531 59 </div> 532 - </div> 533 - <div class="progress-bar-wrap"> 534 - <div class="progress-label"> 535 - <span>0</span> 536 - <span>Rescue analgesia threshold: 4</span> 537 - <span>10</span> 538 - </div> 539 - <div class="progress-track"> 540 - <div class="progress-fill" id="progress-fill"></div> 541 - </div> 542 - <div class="cutoff-indicator"> 543 - <div class="cutoff-line"></div> 544 - <span class="cutoff-label">4</span> 545 - </div> 60 + <small class="fgs-attribution">The Feline Grimace Scale (FGS) <i>© Université de Montréal 2019</i></small> 546 61 </div> 547 - <small class="attribution">The Feline Grimace Scale (FGS) <i>© Université de Montréal 2019</i></small> 62 + 548 63 </div> 549 - 550 64 </div> 551 - 552 - <script> 553 - const units = [ 554 - { 555 - id: 'ears', 556 - label: 'Ear Position', 557 - options: [ 558 - { 559 - score: 0, label: 'Ears facing forward', sublabel: 'Absent', 560 - img: `<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-a-ear-position-0.jpg?t=1771863537" alt="Ear position - absent" loading="lazy">` 561 - }, 562 - { 563 - score: 1, label: 'Ears slightly pulled apart', sublabel: 'Moderately present or uncertain', 564 - img: `<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-a-ear-position-1.jpg?t=1771863537" alt="Ear position - moderately present" loading="lazy">` 565 - }, 566 - { 567 - score: 2, label: 'Ears rotated outwards', sublabel: 'Markedly present', 568 - img: `<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-a-ear-position-2.jpg?t=1771863538" alt="Ear position - markedly present" loading="lazy">` 569 - } 570 - ] 571 - }, 572 - { 573 - id: 'eyes', 574 - label: 'Orbital Tightening', 575 - options: [ 576 - { 577 - score: 0, label: 'Eyes opened', sublabel: 'Absent', 578 - img: `<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-b-orbital-tightening-0.jpg?t=1771863539" alt="Orbital tightening - absent" loading="lazy">` 579 - }, 580 - { 581 - score: 1, label: 'Partially closed eyes', sublabel: 'Moderately present or uncertain', 582 - img: `<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-b-orbital-tightening-1.jpg?t=1771863539" alt="Orbital tightening - moderately present" loading="lazy">` 583 - }, 584 - { 585 - score: 2, label: 'Squinted eyes', sublabel: 'Markedly present', 586 - img: `<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-b-orbital-tightening-2.jpg?t=1771863540" alt="Orbital tightening - markedly present" loading="lazy">` 587 - } 588 - ] 589 - }, 590 - { 591 - id: 'muzzle', 592 - label: 'Muzzle Tension', 593 - options: [ 594 - { 595 - score: 0, label: 'Relaxed (round shape)', sublabel: 'Absent', 596 - img: `<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-c-muzzle-tension-0.jpg?t=1771863541" alt="Muzzle tension - absent" loading="lazy">` 597 - }, 598 - { 599 - score: 1, label: 'Mild tension', sublabel: 'Moderately present or uncertain', 600 - img: `<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-c-muzzle-tension-1.jpg?t=1771863542" alt="Muzzle tension - moderately present" loading="lazy">` 601 - }, 602 - { 603 - score: 2, label: 'Tense (elliptical shape)', sublabel: 'Markedly present', 604 - img: `<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-c-muzzle-tension-2.jpg?t=1771863542" alt="Muzzle tension - markedly present" loading="lazy">` 605 - } 606 - ] 607 - }, 608 - { 609 - id: 'whiskers', 610 - label: 'Whiskers Change', 611 - options: [ 612 - { 613 - score: 0, label: 'Loose and curved', sublabel: 'Absent', 614 - img: `<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-d-whiskers-change-0.jpg?t=1771863543" alt="Whiskers change - absent" loading="lazy">` 615 - }, 616 - { 617 - score: 1, label: 'Slightly curved or straight (closer together)', sublabel: 'Moderately present or uncertain', 618 - img: `<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-d-whiskers-change-1.jpg?t=1771863544" alt="Whiskers change - moderately present" loading="lazy">` 619 - }, 620 - { 621 - score: 2, label: 'Straight and moving forward', sublabel: 'Markedly present', 622 - img: `<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-d-whiskers-change-2.jpg?t=1771863545" alt="Whiskers change - markedly present" loading="lazy">` 623 - } 624 - ] 625 - }, 626 - { 627 - id: 'head', 628 - label: 'Head Position', 629 - options: [ 630 - { 631 - score: 0, label: 'Head above the shoulder line', sublabel: 'Absent', 632 - img: `<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-e-head-position-0.jpg?t=1771863545" alt="Head position - absent" loading="lazy">` 633 - }, 634 - { 635 - score: 1, label: 'Head aligned with the shoulder line', sublabel: 'Moderately present or uncertain', 636 - img: `<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-e-head-position-1.jpg?t=1771863546" alt="Head position - moderately present" loading="lazy">` 637 - }, 638 - { 639 - score: 2, label: 'Head below the shoulder line or tilted down', sublabel: 'Markedly present', 640 - img: `<img src="https://cdn11.bigcommerce.com/s-lh9wfk05w0/images/stencil/320w/image-manager/-e-head-position-2.jpg?t=1771863547" alt="Head position - markedly present" loading="lazy">` 641 - } 642 - ] 643 - } 644 - ]; 645 - 646 - const scores = { ears: null, eyes: null, muzzle: null, whiskers: null, head: null }; 647 - const unitIds = ['ears', 'eyes', 'muzzle', 'whiskers', 'head']; 648 - 649 - function getTotal() { 650 - return unitIds.reduce((sum, id) => sum + (scores[id] ?? 0), 0); 651 - } 652 - 653 - function getColorClass(total) { 654 - if (total === 0) return 'color-0'; 655 - if (total <= 3) return 'color-mild'; 656 - if (total <= 8) return 'color-pain'; 657 - return 'color-severe'; 658 - } 659 - 660 - function getBadgeClass(total) { 661 - if (total === 0) return 'badge-0'; 662 - if (total <= 3) return 'badge-mild'; 663 - if (total <= 8) return 'badge-pain'; 664 - return 'badge-severe'; 665 - } 666 - 667 - function getBadgeLabel(total) { 668 - if (total === 0) return 'No Pain'; 669 - if (total <= 3) return 'Mild / No Pain'; 670 - if (total <= 8) return 'Likely In Pain'; 671 - return 'Severe Pain'; 672 - } 673 - 674 - function getInterpText(total) { 675 - if (total === 0) return 'This cat is not in pain. If you are a cat owner and are concerned, please consult your veterinary surgeon.'; 676 - 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.'; 677 - 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. Clinical judgement will differentiate if the FGS scores are high due to pain, rather than other factors such as stress, fear or sedation.'; 678 - 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. Clinical judgement will differentiate if the FGS scores are high due to pain, rather than other factors such as stress, fear or sedation.'; 679 - } 680 - 681 - function updateScorePanel() { 682 - const total = getTotal(); 683 - const colorClass = getColorClass(total); 684 - 685 - const totalEl = document.getElementById('total-display'); 686 - totalEl.textContent = total; 687 - totalEl.className = `total-number ${colorClass}`; 688 - totalEl.classList.add('pop-animate'); 689 - totalEl.addEventListener('animationend', () => totalEl.classList.remove('pop-animate'), { once: true }); 690 - 691 - const badge = document.getElementById('interp-badge'); 692 - badge.className = `interp-badge ${getBadgeClass(total)}`; 693 - badge.textContent = getBadgeLabel(total); 694 - 695 - document.getElementById('interp-text').textContent = getInterpText(total); 696 - 697 - const fill = document.getElementById('progress-fill'); 698 - const pct = (total / 10) * 100; 699 - fill.style.width = pct + '%'; 700 - fill.className = `progress-fill ${colorClass}`; 701 - 702 - // update breakdown 703 - unitIds.forEach(id => { 704 - const el = document.getElementById(`score-val-${id}`); 705 - if (el) { 706 - el.textContent = scores[id] !== null ? scores[id] : '—'; 707 - el.style.color = scores[id] !== null ? 'var(--fgs-green)' : 'var(--fgs-text-muted)'; 708 - } 709 - }); 710 - 711 - // update unit score badges 712 - unitIds.forEach(id => { 713 - const badge = document.getElementById(`unit-badge-${id}`); 714 - if (badge) { 715 - if (scores[id] !== null) { 716 - badge.textContent = `Score: ${scores[id]}`; 717 - badge.classList.add('selected'); 718 - } else { 719 - badge.textContent = 'Not scored'; 720 - badge.classList.remove('selected'); 721 - } 722 - } 723 - }); 724 - } 725 - 726 - function buildUI() { 727 - const container = document.getElementById('units-container'); 728 - const breakdown = document.getElementById('score-breakdown'); 729 - 730 - // Build breakdown items 731 - const labels = ['Ears', 'Eyes', 'Muzzle', 'Whiskers', 'Head']; 732 - unitIds.forEach((id, i) => { 733 - const item = document.createElement('div'); 734 - item.className = 'score-item'; 735 - item.innerHTML = `<div class="score-item-label">${labels[i]}</div><div class="score-item-value" id="score-val-${id}">—</div>`; 736 - breakdown.appendChild(item); 737 - }); 738 - 739 - // Build unit cards 740 - units.forEach(unit => { 741 - const card = document.createElement('div'); 742 - card.className = 'unit-card'; 743 - card.innerHTML = ` 744 - <div class="unit-header"> 745 - <h2>${unit.label}</h2> 746 - <span class="unit-score-badge" id="unit-badge-${unit.id}">Not scored</span> 747 - </div> 748 - <div class="options-row" role="group" aria-label="${unit.label}"> 749 - ${unit.options.map(opt => ` 750 - <button class="option-btn" data-unit="${unit.id}" data-score="${opt.score}" aria-pressed="false"> 751 - <div class="score-pill">${opt.score}</div> 752 - <div class="cat-illustration">${opt.img}</div> 753 - <div class="option-label">${opt.label}</div> 754 - <div class="option-sublabel">${opt.sublabel}</div> 755 - </button> 756 - `).join('')} 757 - </div> 758 - `; 759 - container.appendChild(card); 760 - }); 761 - 762 - // Event delegation 763 - container.addEventListener('click', e => { 764 - const btn = e.target.closest('.option-btn'); 765 - if (!btn) return; 766 - const unitId = btn.dataset.unit; 767 - const score = parseInt(btn.dataset.score); 768 - 769 - // deselect siblings 770 - const siblings = btn.closest('.options-row').querySelectorAll('.option-btn'); 771 - siblings.forEach(s => { s.classList.remove('selected'); s.setAttribute('aria-pressed', 'false'); }); 772 - 773 - // select this 774 - btn.classList.add('selected'); 775 - btn.setAttribute('aria-pressed', 'true'); 776 - scores[unitId] = score; 777 - 778 - updateScorePanel(); 779 - }); 780 - } 781 - 782 - buildUI(); 783 - updateScorePanel(); 784 - </script> 785 - </body> 786 - 787 65 {{/partial}} 788 - {{> layout/base}} 66 + {{> layout/base}}