2025-06-08 20:31:11 +02:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > Dog Calorie Calculator - Canine Nutrition and Wellness< / title >
< style >
2025-06-15 21:57:39 +02:00
/* Sundog Dog Food Calorie Calculator Styles */
2025-06-26 16:34:38 +02:00
/* CSS Variables for theming */
: root {
--bg-primary : #fdfcfe ;
--bg-secondary : #ffffff ;
--border-color : #e8e3ed ;
--text-primary : #6f3f6d ;
--text-secondary : #8f7a8e ;
2025-06-26 17:19:18 +02:00
--accent-color : #f19a5f ;
--text-label : #635870 ; /* For form labels, secondary UI text */
--success-color : #7fa464 ; /* Green for success states */
--bg-tertiary : #f8f5fa ; /* Light background variant */
--error-color : #e87159 ; /* Error states and messages */
2025-06-26 16:34:38 +02:00
}
2025-06-08 22:27:21 +02:00
2025-06-08 20:31:11 +02:00
body {
margin : 0 ;
padding : 0 ;
background : transparent ;
2025-10-28 09:58:20 +01:00
overflow : hidden ; /* hide internal scrollbars; parent resizes iframe */
2025-06-08 22:27:21 +02:00
font-family : 'Montserrat' , - apple-system , BlinkMacSystemFont , 'Segoe UI' , Roboto , Oxygen , Ubuntu , Cantarell , sans-serif ;
line-height : 1.5 ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 20:31:11 +02:00
}
2025-06-08 22:27:21 +02:00
2025-06-08 20:31:11 +02:00
. dog-calculator-container {
2025-08-18 15:33:44 +02:00
max-width : 640 px ;
2025-06-08 22:27:21 +02:00
margin : 0 auto ;
padding : 24 px ;
box-sizing : border-box ;
2025-06-08 20:31:11 +02:00
opacity : 0 ;
transition : opacity 0.3 s ease ;
}
2025-06-08 22:27:21 +02:00
2025-06-08 20:31:11 +02:00
. dog-calculator-container . loaded {
opacity : 1 ;
}
2025-06-08 22:27:21 +02:00
. dog-calculator-container * ,
. dog-calculator-container * :: before ,
. dog-calculator-container * :: after {
box-sizing : border-box ;
}
. dog-calculator-section {
2025-06-26 17:19:18 +02:00
background : var ( - - bg - primary ) ;
border : 1 px solid var ( - - border - color ) ;
2025-06-08 22:27:21 +02:00
border-radius : 8 px 8 px 0 0 ;
padding : 24 px ;
margin-bottom : 0 ;
box-shadow : 0 3 px 6 px rgba ( 0 , 0 , 0 , 0.08 ) ;
}
. dog-calculator-section-header {
display : flex ;
justify-content : space-between ;
align-items : center ;
margin-bottom : 24 px ;
flex-wrap : wrap ;
gap : 16 px ;
}
. dog-calculator-section h2 {
margin : 0 ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
font-size : 1.5 rem ;
font-weight : 600 ;
}
/* Unit Switch */
. dog-calculator-unit-switch {
display : flex ;
align-items : center ;
gap : 12 px ;
}
. dog-calculator-unit-label {
font-size : 0.9 rem ;
font-weight : 500 ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - label ) ;
2025-06-08 22:27:21 +02:00
transition : color 0.2 s ease ;
}
. dog-calculator-unit-label . active {
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
font-weight : 600 ;
}
. dog-calculator-switch {
position : relative ;
display : inline-block ;
width : 48 px ;
height : 24 px ;
}
. dog-calculator-switch input {
opacity : 0 ;
width : 0 ;
height : 0 ;
}
. dog-calculator-slider {
position : absolute ;
cursor : pointer ;
top : 0 ;
left : 0 ;
right : 0 ;
bottom : 0 ;
2025-06-26 17:19:18 +02:00
background-color : var ( - - border - color ) ;
2025-06-08 22:27:21 +02:00
transition : 0.3 s ;
border-radius : 24 px ;
}
. dog-calculator-slider : before {
position : absolute ;
content : "" ;
height : 18 px ;
width : 18 px ;
left : 3 px ;
bottom : 3 px ;
background-color : white ;
transition : 0.3 s ;
border-radius : 50 % ;
box-shadow : 0 2 px 4 px rgba ( 0 , 0 , 0 , 0.2 ) ;
}
. dog-calculator-switch input : checked + . dog-calculator-slider {
background-color : #f19a5f ;
}
. dog-calculator-switch input : checked + . dog-calculator-slider : before {
transform : translateX ( 24 px ) ;
}
. dog-calculator-form-group {
margin-bottom : 20 px ;
}
. dog-calculator-form-group label {
display : block ;
margin-bottom : 8 px ;
font-weight : 500 ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
font-size : 1 rem ;
}
. dog-calculator-form-group select ,
. dog-calculator-form-group input [ type = "number" ] ,
. dog-calculator-form-group input [ type = "text" ] {
width : 100 % ;
padding : 12 px 16 px ;
2025-06-26 17:19:18 +02:00
border : 1 px solid var ( - - border - color ) ;
2025-06-08 22:27:21 +02:00
border-radius : 6 px ;
font-size : 1 rem ;
font-family : inherit ;
2025-06-26 17:19:18 +02:00
background-color : var ( - - bg - secondary ) ;
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
transition : all 0.2 s ease ;
-webkit- appearance : none ;
-moz- appearance : none ;
appearance : none ;
background-image : url ( "data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236f3f6d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e" ) ;
background-repeat : no-repeat ;
background-position : right 12 px center ;
background-size : 20 px ;
padding-right : 40 px ;
}
. dog-calculator-form-group select option {
2025-06-26 17:19:18 +02:00
background-color : var ( - - bg - secondary ) ;
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-form-group input [ type = "number" ] ,
. dog-calculator-form-group input [ type = "text" ] {
background-image : none ;
padding-right : 16 px ;
}
. dog-calculator-form-group select : focus ,
. dog-calculator-form-group input [ type = "number" ] : focus ,
. dog-calculator-form-group input [ type = "text" ] : focus {
outline : none ;
border-color : #f19a5f ;
2025-06-26 17:19:18 +02:00
background-color : var ( - - bg - secondary ) ;
2025-06-08 22:27:21 +02:00
box-shadow : 0 0 0 3 px rgba ( 241 , 154 , 95 , 0.1 ) ;
}
. dog-calculator-form-group input [ readonly ] {
2025-06-26 17:19:18 +02:00
background-color : var ( - - bg - tertiary ) ;
2025-06-08 22:27:21 +02:00
cursor : not-allowed ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - label ) ;
2025-06-08 22:27:21 +02:00
}
2025-11-12 16:44:43 +01:00
/* Kaya end-weight readonly field: compact, non-editable */
# kayaEndWeight {
width : 120 px ;
display : inline-block ;
}
2025-11-12 17:00:45 +01:00
2025-06-08 22:27:21 +02:00
. dog-calculator-results {
background : linear-gradient ( 135 deg , rgba ( 241 , 154 , 95 , 0.08 ) 0 % , rgba ( 241 , 154 , 95 , 0.04 ) 100 % ) ;
border : 1 px solid rgba ( 241 , 154 , 95 , 0.2 ) ;
border-radius : 6 px ;
padding : 20 px ;
margin-top : 24 px ;
}
. dog-calculator-result-item {
display : flex ;
justify-content : space-between ;
align-items : center ;
margin-bottom : 12 px ;
2025-08-18 15:33:44 +02:00
gap : 10 px ; /* Add gap between label and value */
2025-06-08 22:27:21 +02:00
}
. dog-calculator-result-item : last-child {
margin-bottom : 0 ;
}
. dog-calculator-result-label {
font-weight : 500 ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
font-size : 0.95 rem ;
}
. dog-calculator-result-value {
font-weight : 600 ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
font-size : 1.1 rem ;
padding : 4 px 12 px ;
background : rgba ( 241 , 154 , 95 , 0.15 ) ;
border-radius : 4 px ;
2025-08-18 15:33:44 +02:00
white-space : nowrap ; /* Prevent text from wrapping to multiple lines */
2025-06-08 22:27:21 +02:00
}
. dog-calculator-collapsible {
2025-06-26 17:24:32 +02:00
background : var ( - - bg - primary ) ;
2025-06-26 17:19:18 +02:00
border : 1 px solid var ( - - border - color ) ;
2025-06-08 22:27:21 +02:00
border-top : none ;
margin-bottom : 0 ;
overflow : hidden ;
box-shadow : 0 3 px 6 px rgba ( 0 , 0 , 0 , 0.08 ) ;
}
. dog-calculator-collapsible-header {
2025-06-26 17:19:18 +02:00
background : var ( - - bg - tertiary ) ;
2025-06-08 22:27:21 +02:00
padding : 20 px 24 px ;
2025-06-26 17:19:18 +02:00
border-bottom : 1 px solid var ( - - border - color ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-collapsible-header h3 {
margin : 0 ;
font-size : 1.25 rem ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
font-weight : 600 ;
}
. dog-calculator-collapsible-content {
display : block ;
}
. dog-calculator-collapsible-inner {
padding : 24 px ;
}
. dog-calculator-input-group {
display : flex ;
gap : 16 px ;
align-items : flex-end ;
}
. dog-calculator-input-group . dog-calculator-form-group {
flex : 1 ;
margin-bottom : 0 ;
}
. dog-calculator-unit-select {
min-width : 120 px ;
}
. dog-calculator-error {
2025-06-26 17:19:18 +02:00
color : var ( - - error - color ) ;
2025-06-08 22:27:21 +02:00
font-size : 0.875 rem ;
margin-top : 6 px ;
font-weight : 500 ;
}
. dog-calculator-hidden {
display : none ;
}
/* Action Buttons */
. dog-calculator-action-buttons {
display : flex ;
justify-content : center ;
gap : 16 px ;
padding : 20 px ;
2025-06-26 17:19:18 +02:00
background : var ( - - bg - tertiary ) ;
border-left : 1 px solid var ( - - border - color ) ;
border-right : 1 px solid var ( - - border - color ) ;
2025-06-08 22:27:21 +02:00
margin-top : -1 px ;
2025-06-09 09:58:09 +02:00
box-shadow : 0 3 px 6 px rgba ( 0 , 0 , 0 , 0.08 ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-btn {
padding : 8 px 16 px ;
2025-06-26 17:19:18 +02:00
border : 1 px solid var ( - - border - color ) ;
2025-06-08 22:27:21 +02:00
border-radius : 6 px ;
font-size : 0.9 rem ;
font-weight : 500 ;
font-family : inherit ;
cursor : pointer ;
transition : all 0.2 s ease ;
display : inline-flex ;
align-items : center ;
gap : 6 px ;
background : white ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-btn : hover {
transform : translateY ( -1 px ) ;
box-shadow : 0 2 px 4 px rgba ( 0 , 0 , 0 , 0.1 ) ;
}
. dog-calculator-btn-share : hover {
border-color : #9f5999 ;
color : #9f5999 ;
}
2025-10-28 09:58:20 +01:00
/* Embed button removed */
2025-06-08 22:27:21 +02:00
. dog-calculator-footer {
text-align : center ;
padding : 20 px ;
2025-06-26 17:19:18 +02:00
background : var ( - - bg - primary ) ;
border : 1 px solid var ( - - border - color ) ;
2025-06-08 22:27:21 +02:00
border-radius : 0 0 8 px 8 px ;
border-top : none ;
margin-top : -1 px ;
2025-06-09 09:58:09 +02:00
box-shadow : 0 3 px 6 px rgba ( 0 , 0 , 0 , 0.08 ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-footer a {
color : #9f5999 ;
text-decoration : none ;
font-size : 0.9 rem ;
font-weight : 500 ;
transition : color 0.2 s ease ;
}
. dog-calculator-footer a : hover {
color : #f19a5f ;
text-decoration : underline ;
}
/* Mobile Responsive Design */
@ media ( max-width : 576px ) {
. dog-calculator-container {
padding : 16 px ;
}
. dog-calculator-section ,
. dog-calculator-collapsible-inner {
padding : 20 px ;
}
. dog-calculator-section h2 ,
. dog-calculator-collapsible-header h3 {
font-size : 1.3 rem ;
}
. dog-calculator-section-header {
flex-direction : column ;
align-items : stretch ;
gap : 12 px ;
}
. dog-calculator-section h2 {
text-align : center ;
}
. dog-calculator-unit-switch {
justify-content : center ;
}
. dog-calculator-action-buttons {
flex-direction : column ;
padding : 16 px ;
}
. dog-calculator-btn {
width : 100 % ;
justify-content : center ;
}
. dog-calculator-input-group {
2025-06-26 13:02:28 +02:00
flex-direction : row ;
gap : 12 px ;
align-items : flex-end ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-input-group . dog-calculator-form-group {
2025-06-26 13:02:28 +02:00
margin-bottom : 0 ;
}
/* First form group takes 55%, second takes 40% with some flex */
. dog-calculator-input-group . dog-calculator-form-group : first-child {
flex : 0 0 55 % ;
}
. dog-calculator-input-group . dog-calculator-form-group : last-child {
flex : 1 1 40 % ;
min-width : 100 px ;
2025-06-08 22:27:21 +02:00
}
2025-06-26 13:02:28 +02:00
/* Make sure number inputs don't get too wide */
. dog-calculator-input-group input [ type = "number" ] {
max-width : 100 % ;
}
/* Ensure dropdowns don't overflow their containers */
. dog-calculator-input-group select {
max-width : 100 % ;
overflow : hidden ;
text-overflow : ellipsis ;
}
2025-06-08 22:27:21 +02:00
. dog-calculator-result-item {
flex-direction : column ;
align-items : flex-start ;
gap : 8 px ;
}
. dog-calculator-result-value {
align-self : stretch ;
text-align : center ;
}
. dog-calculator-collapsible-header {
padding : 16 px 20 px ;
}
}
2025-08-18 12:45:44 +02:00
/* Feeding Configuration Styles */
. dog-calculator-container . dog-calculator-feeding-config {
margin-top : 20 px ;
padding : 16 px ;
background : var ( - - bg - tertiary ) ;
border : 1 px solid var ( - - border - color ) ;
border-radius : 8 px ;
}
. dog-calculator-container . dog-calculator-frequency-row {
display : flex ;
align-items : center ;
gap : 16 px ;
flex-wrap : wrap ;
}
. dog-calculator-container . dog-calculator-frequency-row > label {
font-weight : 500 ;
color : var ( - - text - label ) ;
margin : 0 ;
}
. dog-calculator-container . dog-calculator-radio-group {
display : flex ;
gap : 20 px ;
align-items : center ;
}
. dog-calculator-container . dog-calculator-radio-group label {
display : flex ;
align-items : center ;
gap : 6 px ;
cursor : pointer ;
color : var ( - - text - primary ) ;
font-size : 0.95 rem ;
}
. dog-calculator-container . dog-calculator-radio-group input [ type = "radio" ] {
cursor : pointer ;
margin : 0 ;
}
. dog-calculator-container . dog-calculator-radio-group input [ type = "radio" ] : checked + span {
font-weight : 600 ;
color : var ( - - accent - color ) ;
}
. dog-calculator-container . dog-calculator-meal-input {
display : inline-flex ;
align-items : center ;
gap : 6 px ;
2025-08-18 14:36:25 +02:00
margin : 0 auto ;
2025-08-18 12:45:44 +02:00
}
. dog-calculator-container . dog-calculator-meal-input span {
color : var ( - - text - secondary ) ;
font-size : 0.95 rem ;
}
. dog-calculator-container . dog-calculator-meal-input input [ type = "number" ] {
width : 50 px ;
padding : 4 px 8 px ;
border : 1 px solid var ( - - border - color ) ;
border-radius : 4 px ;
font-size : 0.95 rem ;
color : var ( - - text - primary ) ;
background : var ( - - bg - secondary ) ;
text-align : center ;
}
. dog-calculator-container . dog-calculator-meal-input input [ type = "number" ] : focus {
outline : none ;
border-color : var ( - - accent - color ) ;
box-shadow : 0 0 0 2 px rgba ( 241 , 154 , 95 , 0.1 ) ;
}
/* Update meal note styling */
. dog-calculator-container # mealNote {
color : var ( - - text - secondary ) ;
font-size : 0.9 rem ;
font-weight : normal ;
margin-left : 4 px ;
}
/* Mobile responsive adjustments for feeding config */
@ media ( max-width : 480px ) {
. dog-calculator-meal-input {
margin-left : 0 ;
width : 100 % ;
margin-top : 8 px ;
}
. dog-calculator-frequency-row {
flex-direction : column ;
align-items : flex-start ;
}
2025-08-18 15:33:44 +02:00
/* Stack result items vertically on small screens */
. dog-calculator-result-item {
flex-direction : column ;
align-items : flex-start ;
gap : 8 px ;
}
. dog-calculator-result-label {
margin-right : 0 ;
font-size : 0.9 rem ;
}
. dog-calculator-result-value {
font-size : 1 rem ;
align-self : stretch ;
text-align : center ;
}
2025-08-18 12:45:44 +02:00
}
2025-06-08 22:27:21 +02:00
/* Dark theme - manual override */
2025-08-18 09:05:00 +02:00
. dog-calculator-container . theme-dark {
2025-06-26 16:34:38 +02:00
--bg-primary : #24202d ;
--bg-secondary : #312b3b ;
2025-08-18 12:45:44 +02:00
--bg-tertiary : #1f1b26 ;
2025-06-26 16:34:38 +02:00
--border-color : #433c4f ;
--text-primary : #f5f3f7 ;
--text-secondary : #b8b0c2 ;
2025-08-18 12:45:44 +02:00
--text-label : #9f94ae ;
--success-color : #7fa464 ;
--error-color : #e87159 ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-section ,
. dog-calculator-container . theme-dark . dog-calculator-collapsible {
2025-06-26 17:19:18 +02:00
background : var ( - - bg - primary ) ;
border-color : var ( - - border - color ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-collapsible-header {
2025-06-26 17:19:18 +02:00
background : var ( - - bg - secondary ) ;
border-color : var ( - - border - color ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-collapsible-header : hover {
background : #3a3446 ;
}
. dog-calculator-container . theme-dark . dog-calculator-section h2 ,
. dog-calculator-container . theme-dark . dog-calculator-collapsible-header h3 ,
. dog-calculator-container . theme-dark . dog-calculator-form-group label ,
. dog-calculator-container . theme-dark . dog-calculator-result-label {
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-unit-label {
2025-06-26 17:19:18 +02:00
color : var ( - - text - secondary )
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-unit-label . active {
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-slider {
2025-06-26 17:19:18 +02:00
background-color : var ( - - border - color ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-form-group select ,
. dog-calculator-container . theme-dark . dog-calculator-form-group input [ type = "number" ] ,
. dog-calculator-container . theme-dark . dog-calculator-form-group input [ type = "text" ] {
2025-06-26 17:19:18 +02:00
background-color : var ( - - bg - secondary ) ;
border-color : var ( - - border - color ) ;
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
background-image : url ( "data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f5f3f7' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e" ) ;
}
. dog-calculator-container . theme-dark . dog-calculator-form-group select option {
2025-06-26 17:19:18 +02:00
background-color : var ( - - bg - secondary ) ;
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-form-group select : focus ,
. dog-calculator-container . theme-dark . dog-calculator-form-group input [ type = "number" ] : focus ,
. dog-calculator-container . theme-dark . dog-calculator-form-group input [ type = "text" ] : focus {
2025-06-26 17:19:18 +02:00
background-color : var ( - - bg - secondary ) ;
2025-06-08 22:27:21 +02:00
border-color : #f19a5f ;
}
. dog-calculator-container . theme-dark . dog-calculator-form-group input [ readonly ] {
2025-06-26 17:19:18 +02:00
background-color : var ( - - border - color ) ;
color : var ( - - text - secondary )
2025-06-08 22:27:21 +02:00
}
2025-06-26 15:55:39 +02:00
. dog-calculator-container . theme-dark . dog-calculator-inline-unit {
2025-06-26 17:19:18 +02:00
background-color : var ( - - bg - secondary ) ;
2025-06-26 15:55:39 +02:00
border-color : rgba ( 241 , 154 , 95 , 0.5 ) ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-26 15:55:39 +02:00
box-shadow : 0 2 px 4 px rgba ( 0 , 0 , 0 , 0.3 ) ;
background-image : url ( "data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f5f3f7' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e" ) ;
}
. dog-calculator-container . theme-dark . dog-calculator-inline-unit : hover {
border-color : #f19a5f ;
box-shadow : 0 2 px 6 px rgba ( 241 , 154 , 95 , 0.3 ) ;
}
. dog-calculator-container . theme-dark . dog-calculator-inline-unit : focus {
border-color : #f19a5f ;
box-shadow : 0 0 0 3 px rgba ( 241 , 154 , 95 , 0.15 ) ;
}
. dog-calculator-container . theme-dark . dog-calculator-inline-unit option {
2025-06-26 17:19:18 +02:00
background-color : var ( - - bg - secondary ) ;
color : var ( - - text - primary ) ;
2025-06-26 15:55:39 +02:00
}
2025-06-26 16:34:38 +02:00
. dog-calculator-container . theme-dark . dog-calculator-unit-btn {
2025-06-26 17:19:18 +02:00
background-color : var ( - - bg - secondary ) ;
border-color : var ( - - border - color ) ;
color : var ( - - text - primary ) ;
2025-06-26 16:34:38 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-unit-btn : hover {
border-color : #f19a5f ;
background : rgba ( 241 , 154 , 95 , 0.2 ) ;
}
. dog-calculator-container . theme-dark . dog-calculator-unit-btn . active {
border-color : #f19a5f ;
background : #f19a5f ;
color : white ;
}
2025-08-18 14:36:25 +02:00
. dog-calculator-container . theme-dark . dog-calculator-unit-btn : disabled {
opacity : 0.5 ;
cursor : not-allowed ;
border-color : var ( - - border - color ) ;
background : var ( - - bg - tertiary ) ;
color : var ( - - text - secondary ) ;
}
. dog-calculator-container . theme-dark . dog-calculator-unit-btn : disabled : hover {
border-color : var ( - - border - color ) ;
background : var ( - - bg - tertiary ) ;
}
2025-06-08 22:27:21 +02:00
. dog-calculator-container . theme-dark . dog-calculator-results {
background : linear-gradient ( 135 deg , rgba ( 241 , 154 , 95 , 0.15 ) 0 % , rgba ( 241 , 154 , 95 , 0.08 ) 100 % ) ;
border-color : rgba ( 241 , 154 , 95 , 0.3 ) ;
}
. dog-calculator-container . theme-dark . dog-calculator-result-value {
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
background : rgba ( 241 , 154 , 95 , 0.2 ) ;
}
. dog-calculator-container . theme-dark . dog-calculator-footer {
2025-06-26 17:19:18 +02:00
background : var ( - - bg - primary ) ;
border-color : var ( - - border - color ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-action-buttons {
2025-06-26 17:19:18 +02:00
background : var ( - - bg - secondary ) ;
border-color : var ( - - border - color ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-btn {
2025-06-26 17:19:18 +02:00
background : var ( - - border - color ) ;
border-color : var ( - - border - color ) ;
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-btn : hover {
background : #524a5f ;
border-color : #524a5f ;
}
. dog-calculator-container . theme-dark . dog-calculator-btn-share : hover {
border-color : #9f5999 ;
color : #f19a5f ;
}
2025-10-28 09:58:20 +01:00
/* Embed button removed */
2025-06-08 22:27:21 +02:00
2025-08-18 12:45:44 +02:00
/* Dark theme feeding configuration styles */
. dog-calculator-container . theme-dark . dog-calculator-feeding-config {
background : var ( - - bg - tertiary ) ;
border-color : var ( - - border - color ) ;
}
. dog-calculator-container . theme-dark . dog-calculator-frequency-row > label {
color : var ( - - text - label ) ;
}
. dog-calculator-container . theme-dark . dog-calculator-radio-group label {
color : var ( - - text - primary ) ;
}
. dog-calculator-container . theme-dark . dog-calculator-radio-group input [ type = "radio" ] : checked + span {
color : #f19a5f ;
}
. dog-calculator-container . theme-dark . dog-calculator-meal-input span {
color : var ( - - text - secondary ) ;
}
. dog-calculator-container . theme-dark . dog-calculator-meal-input input [ type = "number" ] {
background : var ( - - bg - secondary ) ;
border-color : var ( - - border - color ) ;
color : var ( - - text - primary ) ;
}
. dog-calculator-container . theme-dark . dog-calculator-meal-input input [ type = "number" ] : focus {
border-color : #f19a5f ;
box-shadow : 0 0 0 2 px rgba ( 241 , 154 , 95 , 0.15 ) ;
}
. dog-calculator-container . theme-dark # mealNote {
color : var ( - - text - secondary ) ;
}
2025-06-08 22:27:21 +02:00
/* System theme - follows user's OS preference */
@ media ( prefers-color-scheme : dark ) {
. dog-calculator-container . theme-system {
2025-06-26 16:34:38 +02:00
--bg-primary : #24202d ;
--bg-secondary : #312b3b ;
2025-08-18 12:45:44 +02:00
--bg-tertiary : #1f1b26 ;
2025-06-26 16:34:38 +02:00
--border-color : #433c4f ;
--text-primary : #f5f3f7 ;
--text-secondary : #b8b0c2 ;
2025-08-18 12:45:44 +02:00
--text-label : #9f94ae ;
--success-color : #7fa464 ;
--error-color : #e87159 ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-section ,
. dog-calculator-container . theme-system . dog-calculator-collapsible {
2025-06-26 17:19:18 +02:00
background : var ( - - bg - primary ) ;
border-color : var ( - - border - color ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-collapsible-header {
2025-06-26 17:19:18 +02:00
background : var ( - - bg - secondary ) ;
border-color : var ( - - border - color ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-collapsible-header : hover {
background : #3a3446 ;
}
. dog-calculator-container . theme-system . dog-calculator-section h2 ,
. dog-calculator-container . theme-system . dog-calculator-collapsible-header h3 ,
. dog-calculator-container . theme-system . dog-calculator-form-group label ,
. dog-calculator-container . theme-system . dog-calculator-result-label {
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-unit-label {
2025-06-26 17:19:18 +02:00
color : var ( - - text - secondary )
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-unit-label . active {
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-slider {
2025-06-26 17:19:18 +02:00
background-color : var ( - - border - color ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-form-group select ,
. dog-calculator-container . theme-system . dog-calculator-form-group input [ type = "number" ] ,
. dog-calculator-container . theme-system . dog-calculator-form-group input [ type = "text" ] {
2025-06-26 17:19:18 +02:00
background-color : var ( - - bg - secondary ) ;
border-color : var ( - - border - color ) ;
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
background-image : url ( "data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f5f3f7' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e" ) ;
}
. dog-calculator-container . theme-system . dog-calculator-form-group select option {
2025-06-26 17:19:18 +02:00
background-color : var ( - - bg - secondary ) ;
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-form-group select : focus ,
. dog-calculator-container . theme-system . dog-calculator-form-group input [ type = "number" ] : focus ,
. dog-calculator-container . theme-system . dog-calculator-form-group input [ type = "text" ] : focus {
2025-06-26 17:19:18 +02:00
background-color : var ( - - bg - secondary ) ;
2025-06-08 22:27:21 +02:00
border-color : #f19a5f ;
}
. dog-calculator-container . theme-system . dog-calculator-form-group input [ readonly ] {
2025-06-26 17:19:18 +02:00
background-color : var ( - - border - color ) ;
color : var ( - - text - secondary )
2025-06-08 22:27:21 +02:00
}
2025-06-26 15:55:39 +02:00
. dog-calculator-container . theme-system . dog-calculator-inline-unit {
2025-06-26 17:19:18 +02:00
background-color : var ( - - bg - secondary ) ;
2025-06-26 15:55:39 +02:00
border-color : rgba ( 241 , 154 , 95 , 0.5 ) ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-26 15:55:39 +02:00
box-shadow : 0 2 px 4 px rgba ( 0 , 0 , 0 , 0.3 ) ;
background-image : url ( "data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f5f3f7' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e" ) ;
}
. dog-calculator-container . theme-system . dog-calculator-inline-unit : hover {
border-color : #f19a5f ;
box-shadow : 0 2 px 6 px rgba ( 241 , 154 , 95 , 0.3 ) ;
}
. dog-calculator-container . theme-system . dog-calculator-inline-unit : focus {
border-color : #f19a5f ;
box-shadow : 0 0 0 3 px rgba ( 241 , 154 , 95 , 0.15 ) ;
}
. dog-calculator-container . theme-system . dog-calculator-inline-unit option {
2025-06-26 17:19:18 +02:00
background-color : var ( - - bg - secondary ) ;
color : var ( - - text - primary ) ;
2025-06-26 15:55:39 +02:00
}
2025-06-26 16:34:38 +02:00
. dog-calculator-container . theme-system . dog-calculator-unit-btn {
2025-06-26 17:19:18 +02:00
background-color : var ( - - bg - secondary ) ;
border-color : var ( - - border - color ) ;
color : var ( - - text - primary ) ;
2025-06-26 16:34:38 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-unit-btn : hover {
border-color : #f19a5f ;
background : rgba ( 241 , 154 , 95 , 0.2 ) ;
}
. dog-calculator-container . theme-system . dog-calculator-unit-btn . active {
border-color : #f19a5f ;
background : #f19a5f ;
color : white ;
}
2025-08-18 14:36:25 +02:00
. dog-calculator-container . theme-system . dog-calculator-unit-btn : disabled {
opacity : 0.5 ;
cursor : not-allowed ;
border-color : var ( - - border - color ) ;
background : var ( - - bg - tertiary ) ;
color : var ( - - text - secondary ) ;
}
. dog-calculator-container . theme-system . dog-calculator-unit-btn : disabled : hover {
border-color : var ( - - border - color ) ;
background : var ( - - bg - tertiary ) ;
}
2025-06-08 22:27:21 +02:00
. dog-calculator-container . theme-system . dog-calculator-results {
background : linear-gradient ( 135 deg , rgba ( 241 , 154 , 95 , 0.15 ) 0 % , rgba ( 241 , 154 , 95 , 0.08 ) 100 % ) ;
border-color : rgba ( 241 , 154 , 95 , 0.3 ) ;
}
. dog-calculator-container . theme-system . dog-calculator-result-value {
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
background : rgba ( 241 , 154 , 95 , 0.2 ) ;
}
. dog-calculator-container . theme-system . dog-calculator-footer {
2025-06-26 17:19:18 +02:00
background : var ( - - bg - primary ) ;
border-color : var ( - - border - color ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-action-buttons {
2025-06-26 17:19:18 +02:00
background : var ( - - bg - secondary ) ;
border-color : var ( - - border - color ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-btn {
2025-06-26 17:19:18 +02:00
background : var ( - - border - color ) ;
border-color : var ( - - border - color ) ;
color : var ( - - text - primary ) ;
2025-06-08 22:27:21 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-btn : hover {
background : #524a5f ;
border-color : #524a5f ;
}
. dog-calculator-container . theme-system . dog-calculator-btn-share : hover {
border-color : #9f5999 ;
color : #f19a5f ;
}
2025-10-28 09:58:20 +01:00
/* Embed button removed */
2025-08-18 12:45:44 +02:00
/* System theme feeding configuration styles in dark mode */
. dog-calculator-container . theme-system . dog-calculator-feeding-config {
background : var ( - - bg - tertiary ) ;
border-color : var ( - - border - color ) ;
}
. dog-calculator-container . theme-system . dog-calculator-frequency-row > label {
color : var ( - - text - label ) ;
}
. dog-calculator-container . theme-system . dog-calculator-radio-group label {
color : var ( - - text - primary ) ;
}
. dog-calculator-container . theme-system . dog-calculator-radio-group input [ type = "radio" ] : checked + span {
color : #f19a5f ;
}
. dog-calculator-container . theme-system . dog-calculator-meal-input span {
color : var ( - - text - secondary ) ;
}
. dog-calculator-container . theme-system . dog-calculator-meal-input input [ type = "number" ] {
background : var ( - - bg - secondary ) ;
border-color : var ( - - border - color ) ;
color : var ( - - text - primary ) ;
}
. dog-calculator-container . theme-system . dog-calculator-meal-input input [ type = "number" ] : focus {
border-color : #f19a5f ;
box-shadow : 0 0 0 2 px rgba ( 241 , 154 , 95 , 0.15 ) ;
}
. dog-calculator-container . theme-system # mealNote {
color : var ( - - text - secondary ) ;
}
2025-06-08 22:27:21 +02:00
}
2025-06-08 23:45:32 +02:00
/* Modal Styles */
. dog-calculator-modal {
2025-10-28 09:58:20 +01:00
display : none ; /* set to flex via JS when opened */
2025-06-08 23:45:32 +02:00
position : fixed ;
z-index : 10000 ;
left : 0 ;
top : 0 ;
width : 100 % ;
height : 100 % ;
2025-10-28 09:58:20 +01:00
padding : 20 px ;
box-sizing : border-box ;
overflow : auto ; /* allow modal content scroll if needed */
align-items : center ;
justify-content : center ;
2025-06-08 23:45:32 +02:00
animation : fadeIn 0.3 s ease ;
}
@ keyframes fadeIn {
from { opacity : 0 ; }
to { opacity : 1 ; }
}
. dog-calculator-modal-content {
position : relative ;
2025-06-26 17:19:18 +02:00
background-color : var ( - - bg - secondary ) ;
2025-10-28 09:58:20 +01:00
margin : 0 ;
2025-06-08 23:45:32 +02:00
padding : 30 px ;
2025-06-26 17:19:18 +02:00
border : 1 px solid var ( - - border - color ) ;
2025-06-08 23:45:32 +02:00
border-radius : 12 px ;
width : 90 % ;
max-width : 500 px ;
2025-10-28 09:58:20 +01:00
max-height : 90 vh ; /* ensure it fits viewport */
overflow : auto ;
2025-06-08 23:45:32 +02:00
box-shadow : 0 8 px 24 px rgba ( 0 , 0 , 0 , 0.15 ) ;
animation : slideIn 0.3 s ease ;
}
2025-10-28 09:58:20 +01:00
/* Embed modal removed */
2025-06-08 23:45:32 +02:00
@ keyframes slideIn {
from {
opacity : 0 ;
transform : translateY ( -20 px ) ;
}
to {
opacity : 1 ;
transform : translateY ( 0 ) ;
}
}
. dog-calculator-modal-close {
position : absolute ;
right : 20 px ;
top : 20 px ;
font-size : 28 px ;
font-weight : 300 ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 23:45:32 +02:00
cursor : pointer ;
transition : color 0.2 s ease ;
}
. dog-calculator-modal-close : hover {
color : #f19a5f ;
}
. dog-calculator-modal h3 {
margin : 0 0 24 px 0 ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 23:45:32 +02:00
font-size : 1.5 rem ;
}
/* Share Modal */
. dog-calculator-share-buttons {
display : grid ;
grid-template-columns : repeat ( auto - fit , minmax ( 120 px , 1 fr ) ) ;
gap : 12 px ;
margin-bottom : 20 px ;
}
. dog-calculator-share-btn {
padding : 12 px 16 px ;
border : none ;
border-radius : 6 px ;
font-size : 0.9 rem ;
font-weight : 500 ;
color : white ;
cursor : pointer ;
transition : all 0.2 s ease ;
display : flex ;
align-items : center ;
justify-content : center ;
gap : 8 px ;
font-family : inherit ;
}
. dog-calculator-share-facebook { background : #1877f2 ; }
. dog-calculator-share-facebook : hover { background : #1664d1 ; transform : translateY ( -1 px ) ; }
. dog-calculator-share-twitter { background : #1da1f2 ; }
. dog-calculator-share-twitter : hover { background : #1991da ; transform : translateY ( -1 px ) ; }
. dog-calculator-share-linkedin { background : #0a66c2 ; }
. dog-calculator-share-linkedin : hover { background : #084d95 ; transform : translateY ( -1 px ) ; }
2025-06-26 18:03:39 +02:00
. dog-calculator-share-email { background : #6f3f6d ; }
2025-06-08 23:45:32 +02:00
. dog-calculator-share-email : hover { background : #5a3357 ; transform : translateY ( -1 px ) ; }
. dog-calculator-share-copy { background : #f19a5f ; }
. dog-calculator-share-copy : hover { background : #e87741 ; transform : translateY ( -1 px ) ; }
2025-06-09 10:12:28 +02:00
. dog-calculator-share-url {
display : flex ;
width : 100 % ;
}
2025-06-08 23:45:32 +02:00
. dog-calculator-share-url input {
flex : 1 ;
2025-06-09 10:12:28 +02:00
width : 100 % ;
2025-06-08 23:45:32 +02:00
padding : 10 px 16 px ;
2025-06-26 17:19:18 +02:00
border : 1 px solid var ( - - border - color ) ;
2025-06-08 23:45:32 +02:00
border-radius : 6 px ;
font-size : 0.9 rem ;
font-family : monospace ;
2025-06-26 17:19:18 +02:00
background : var ( - - bg - tertiary ) ;
color : var ( - - text - primary ) ;
2025-06-08 23:45:32 +02:00
}
2025-10-28 09:58:20 +01:00
/* Embed UI removed */
2025-06-08 23:45:32 +02:00
/* Dark theme modal styles */
. dog-calculator-container . theme-dark . dog-calculator-modal-content {
2025-06-26 17:19:18 +02:00
background-color : var ( - - bg - primary ) ;
border-color : var ( - - border - color ) ;
2025-06-08 23:45:32 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-modal h3 {
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 23:45:32 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-modal-close {
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 23:45:32 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-modal-close : hover {
color : #f19a5f ;
}
. dog-calculator-container . theme-dark . dog-calculator-share-url input {
2025-06-26 17:19:18 +02:00
background : var ( - - bg - secondary ) ;
border-color : var ( - - border - color ) ;
color : var ( - - text - primary ) ;
2025-06-08 23:45:32 +02:00
}
2025-10-28 09:58:20 +01:00
/* Embed UI removed for dark theme */
2025-06-08 23:45:32 +02:00
/* System theme modal styles */
@ media ( prefers-color-scheme : dark ) {
. dog-calculator-container . theme-system . dog-calculator-modal-content {
2025-06-26 17:19:18 +02:00
background-color : var ( - - bg - primary ) ;
border-color : var ( - - border - color ) ;
2025-06-08 23:45:32 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-modal h3 {
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 23:45:32 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-modal-close {
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-08 23:45:32 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-modal-close : hover {
color : #f19a5f ;
}
. dog-calculator-container . theme-system . dog-calculator-share-url input {
2025-06-26 17:19:18 +02:00
background : var ( - - bg - secondary ) ;
border-color : var ( - - border - color ) ;
color : var ( - - text - primary ) ;
2025-06-08 23:45:32 +02:00
}
2025-10-28 09:58:20 +01:00
/* Embed UI removed for system theme */
2025-06-08 23:45:32 +02:00
}
2025-06-26 11:09:29 +02:00
/* Multi-Food Source Styles */
. dog-calculator-food-sources {
display : flex ;
flex-direction : column ;
gap : 16 px ;
}
. dog-calculator-food-source-card {
2025-06-26 17:19:18 +02:00
background : var ( - - bg - secondary ) ;
border : 1 px solid var ( - - border - color ) ;
2025-06-26 11:09:29 +02:00
border-radius : 8 px ;
padding : 20 px ;
position : relative ;
transition : all 0.2 s ease ;
}
. dog-calculator-food-source-card : hover {
box-shadow : 0 2 px 8 px rgba ( 0 , 0 , 0 , 0.1 ) ;
}
. dog-calculator-food-source-header {
display : flex ;
justify-content : space-between ;
align-items : center ;
margin-bottom : 16 px ;
}
. dog-calculator-food-source-title {
font-weight : 600 ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-26 11:09:29 +02:00
font-size : 1.1 rem ;
margin : 0 ;
}
. dog-calculator-remove-food-btn {
2025-06-26 17:19:18 +02:00
background : var ( - - error - color ) ;
2025-06-26 11:09:29 +02:00
color : white ;
border : none ;
border-radius : 50 % ;
width : 28 px ;
height : 28 px ;
display : flex ;
align-items : center ;
justify-content : center ;
cursor : pointer ;
font-size : 16 px ;
font-weight : 600 ;
transition : all 0.2 s ease ;
line-height : 1 ;
}
. dog-calculator-remove-food-btn : hover {
background : #d65a47 ;
transform : scale ( 1.1 ) ;
}
. dog-calculator-percentage-group {
margin-top : 16 px ;
padding-top : 16 px ;
2025-06-26 17:19:18 +02:00
border-top : 1 px solid var ( - - border - color ) ;
2025-06-26 11:09:29 +02:00
}
. dog-calculator-percentage-label {
display : block ;
margin-bottom : 8 px ;
font-weight : 500 ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-26 11:09:29 +02:00
font-size : 1 rem ;
}
. dog-calculator-percentage-input-group {
display : flex ;
align-items : center ;
gap : 12 px ;
}
. dog-calculator-percentage-slider {
flex : 1 ;
height : 6 px ;
border-radius : 3 px ;
2025-06-26 17:19:18 +02:00
background : var ( - - border - color ) ;
2025-06-26 11:09:29 +02:00
outline : none ;
transition : all 0.2 s ease ;
-webkit- appearance : none ;
appearance : none ;
}
. dog-calculator-percentage-slider :: -webkit-slider-thumb {
-webkit- appearance : none ;
appearance : none ;
width : 20 px ;
height : 20 px ;
border-radius : 50 % ;
background : #f19a5f ;
cursor : pointer ;
border : 2 px solid white ;
box-shadow : 0 2 px 4 px rgba ( 0 , 0 , 0 , 0.2 ) ;
transition : all 0.2 s ease ;
}
. dog-calculator-percentage-slider :: -webkit-slider-thumb : hover {
background : #e87741 ;
transform : scale ( 1.1 ) ;
}
. dog-calculator-percentage-slider :: -moz-range-thumb {
width : 20 px ;
height : 20 px ;
border-radius : 50 % ;
background : #f19a5f ;
cursor : pointer ;
border : 2 px solid white ;
box-shadow : 0 2 px 4 px rgba ( 0 , 0 , 0 , 0.2 ) ;
transition : all 0.2 s ease ;
}
. dog-calculator-percentage-input {
width : 70 px ;
padding : 8 px 12 px ;
2025-06-26 17:19:18 +02:00
border : 1 px solid var ( - - border - color ) ;
2025-06-26 11:09:29 +02:00
border-radius : 6 px ;
font-size : 0.9 rem ;
text-align : center ;
2025-06-26 17:19:18 +02:00
background-color : var ( - - bg - secondary ) ;
color : var ( - - text - primary ) ;
2025-06-26 11:09:29 +02:00
}
. dog-calculator-percentage-input : focus {
outline : none ;
border-color : #f19a5f ;
box-shadow : 0 0 0 3 px rgba ( 241 , 154 , 95 , 0.1 ) ;
}
. dog-calculator-add-food-btn {
display : flex ;
align-items : center ;
justify-content : center ;
gap : 8 px ;
width : 100 % ;
padding : 16 px ;
2025-06-26 17:19:18 +02:00
border : 2 px dashed var ( - - border - color ) ;
2025-06-26 11:09:29 +02:00
border-radius : 8 px ;
background : transparent ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - label ) ;
2025-06-26 11:09:29 +02:00
font-size : 1 rem ;
font-weight : 500 ;
cursor : pointer ;
transition : all 0.2 s ease ;
font-family : inherit ;
margin-top : 16 px ;
}
. dog-calculator-add-food-btn : hover {
border-color : #f19a5f ;
color : #f19a5f ;
background : rgba ( 241 , 154 , 95 , 0.05 ) ;
}
. dog-calculator-add-food-btn : disabled {
opacity : 0.5 ;
cursor : not-allowed ;
2025-06-26 17:19:18 +02:00
border-color : var ( - - border - color ) ;
color : var ( - - text - label ) ;
2025-06-26 11:09:29 +02:00
background : transparent ;
}
. dog-calculator-add-food-btn : disabled : hover {
2025-06-26 17:19:18 +02:00
border-color : var ( - - border - color ) ;
color : var ( - - text - label ) ;
2025-06-26 11:09:29 +02:00
background : transparent ;
}
. dog-calculator-food-results {
background : linear-gradient ( 135 deg , rgba ( 241 , 154 , 95 , 0.08 ) 0 % , rgba ( 241 , 154 , 95 , 0.04 ) 100 % ) ;
border : 1 px solid rgba ( 241 , 154 , 95 , 0.2 ) ;
border-radius : 6 px ;
padding : 16 px ;
margin-top : 20 px ;
}
. dog-calculator-food-result-item {
display : flex ;
justify-content : space-between ;
align-items : center ;
margin-bottom : 8 px ;
font-size : 0.9 rem ;
}
. dog-calculator-food-result-item : last-child {
margin-bottom : 0 ;
}
. dog-calculator-food-result-label {
font-weight : 500 ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-26 11:09:29 +02:00
}
. dog-calculator-food-result-value {
font-weight : 600 ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-26 11:09:29 +02:00
padding : 2 px 8 px ;
background : rgba ( 241 , 154 , 95 , 0.15 ) ;
border-radius : 3 px ;
font-size : 0.85 rem ;
}
/* Dark theme support for food sources */
. dog-calculator-container . theme-dark . dog-calculator-food-source-card {
2025-06-26 17:19:18 +02:00
background : var ( - - bg - secondary ) ;
border-color : var ( - - border - color ) ;
2025-06-26 11:09:29 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-food-source-title {
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-26 11:09:29 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-percentage-label {
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-26 11:09:29 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-percentage-slider {
2025-06-26 17:19:18 +02:00
background : var ( - - border - color ) ;
2025-06-26 11:09:29 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-percentage-input {
2025-06-26 17:19:18 +02:00
background : var ( - - border - color ) ;
2025-06-26 11:09:29 +02:00
border-color : #524a5f ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-26 11:09:29 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-percentage-group {
2025-06-26 17:19:18 +02:00
border-color : var ( - - border - color ) ;
2025-06-26 11:09:29 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-add-food-btn {
2025-06-26 17:19:18 +02:00
border-color : var ( - - border - color ) ;
color : var ( - - text - secondary )
2025-06-26 11:09:29 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-add-food-btn : hover {
border-color : #f19a5f ;
color : #f19a5f ;
background : rgba ( 241 , 154 , 95 , 0.1 ) ;
}
. dog-calculator-container . theme-dark . dog-calculator-food-results {
background : linear-gradient ( 135 deg , rgba ( 241 , 154 , 95 , 0.15 ) 0 % , rgba ( 241 , 154 , 95 , 0.08 ) 100 % ) ;
border-color : rgba ( 241 , 154 , 95 , 0.3 ) ;
}
. dog-calculator-container . theme-dark . dog-calculator-food-result-label {
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-26 11:09:29 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-food-result-value {
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-26 11:09:29 +02:00
background : rgba ( 241 , 154 , 95 , 0.2 ) ;
}
/* System theme support for food sources */
@ media ( prefers-color-scheme : dark ) {
. dog-calculator-container . theme-system . dog-calculator-food-source-card {
2025-06-26 17:19:18 +02:00
background : var ( - - bg - secondary ) ;
border-color : var ( - - border - color ) ;
2025-06-26 11:09:29 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-food-source-title {
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-26 11:09:29 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-percentage-label {
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-26 11:09:29 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-percentage-slider {
2025-06-26 17:19:18 +02:00
background : var ( - - border - color ) ;
2025-06-26 11:09:29 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-percentage-input {
2025-06-26 17:19:18 +02:00
background : var ( - - border - color ) ;
2025-06-26 11:09:29 +02:00
border-color : #524a5f ;
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-26 11:09:29 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-percentage-group {
2025-06-26 17:19:18 +02:00
border-color : var ( - - border - color ) ;
2025-06-26 11:09:29 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-add-food-btn {
2025-06-26 17:19:18 +02:00
border-color : var ( - - border - color ) ;
color : var ( - - text - secondary )
2025-06-26 11:09:29 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-add-food-btn : hover {
border-color : #f19a5f ;
color : #f19a5f ;
background : rgba ( 241 , 154 , 95 , 0.1 ) ;
}
. dog-calculator-container . theme-system . dog-calculator-food-results {
background : linear-gradient ( 135 deg , rgba ( 241 , 154 , 95 , 0.15 ) 0 % , rgba ( 241 , 154 , 95 , 0.08 ) 100 % ) ;
border-color : rgba ( 241 , 154 , 95 , 0.3 ) ;
}
. dog-calculator-container . theme-system . dog-calculator-food-result-label {
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-26 11:09:29 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-food-result-value {
2025-06-26 17:19:18 +02:00
color : var ( - - text - primary ) ;
2025-06-26 11:09:29 +02:00
background : rgba ( 241 , 154 , 95 , 0.2 ) ;
}
}
/* Mobile responsive design for food sources */
@ media ( max-width : 576px ) {
. dog-calculator-food-source-card {
padding : 16 px ;
}
. dog-calculator-food-source-header {
flex-direction : column ;
align-items : flex-start ;
gap : 12 px ;
}
. dog-calculator-remove-food-btn {
align-self : flex-end ;
margin-top : -8 px ;
}
. dog-calculator-percentage-input-group {
flex-direction : column ;
gap : 8 px ;
align-items : stretch ;
}
. dog-calculator-percentage-input {
width : 100 % ;
}
. dog-calculator-add-food-btn {
padding : 12 px ;
font-size : 0.9 rem ;
}
. dog-calculator-food-result-item {
flex-direction : column ;
align-items : flex-start ;
gap : 4 px ;
}
. dog-calculator-food-result-value {
align-self : stretch ;
text-align : center ;
}
}
2025-06-26 11:19:29 +02:00
/* Lock Icon Styles */
. dog-calculator-lock-icon {
display : inline-block ;
width : 16 px ;
height : 16 px ;
margin-left : 8 px ;
cursor : pointer ;
font-size : 14 px ;
line-height : 1 ;
vertical-align : middle ;
transition : all 0.2 s ease ;
user-select : none ;
opacity : 0.6 ;
}
. dog-calculator-lock-icon : hover {
opacity : 1 ;
transform : scale ( 1.1 ) ;
}
. dog-calculator-lock-icon . locked {
color : #f19a5f ;
opacity : 1 ;
font-weight : bold ;
}
. dog-calculator-lock-icon . unlocked {
2025-06-26 17:19:18 +02:00
color : var ( - - text - label ) ;
2025-06-26 11:19:29 +02:00
}
. dog-calculator-lock-icon . disabled {
opacity : 0.3 ;
cursor : not-allowed ;
}
. dog-calculator-lock-icon . disabled : hover {
opacity : 0.3 ;
transform : none ;
}
/* Dark theme support for lock icons */
. dog-calculator-container . theme-dark . dog-calculator-lock-icon . unlocked {
2025-06-26 17:19:18 +02:00
color : var ( - - text - secondary )
2025-06-26 11:19:29 +02:00
}
. dog-calculator-container . theme-dark . dog-calculator-lock-icon . locked {
color : #f19a5f ;
}
/* System theme support for lock icons */
@ media ( prefers-color-scheme : dark ) {
. dog-calculator-container . theme-system . dog-calculator-lock-icon . unlocked {
2025-06-26 17:19:18 +02:00
color : var ( - - text - secondary )
2025-06-26 11:19:29 +02:00
}
. dog-calculator-container . theme-system . dog-calculator-lock-icon . locked {
color : #f19a5f ;
}
}
2025-06-26 11:27:03 +02:00
/* Disabled slider and input styles */
. dog-calculator-percentage-slider : disabled {
opacity : 0.5 ;
cursor : not-allowed ;
background : #f0f0f0 ;
2025-06-26 11:47:33 +02:00
pointer-events : none ;
2025-06-26 11:27:03 +02:00
}
. dog-calculator-percentage-slider : disabled :: -webkit-slider-thumb {
background : #ccc ;
cursor : not-allowed ;
}
. dog-calculator-percentage-slider : disabled :: -webkit-slider-thumb : hover {
background : #ccc ;
transform : none ;
}
. dog-calculator-percentage-slider : disabled :: -moz-range-thumb {
background : #ccc ;
cursor : not-allowed ;
}
. dog-calculator-percentage-input : disabled {
opacity : 0.5 ;
cursor : not-allowed ;
background-color : #f8f8f8 ;
border-color : #ddd ;
2025-06-26 11:47:33 +02:00
pointer-events : none ;
2025-06-26 11:27:03 +02:00
}
/* Dark theme disabled styles */
. dog-calculator-container . theme-dark . dog-calculator-percentage-slider : disabled {
background : #2a2530 ;
}
. dog-calculator-container . theme-dark . dog-calculator-percentage-input : disabled {
background-color : #2a2530 ;
border-color : #3a3442 ;
color : #8a8a8a ;
}
/* System theme disabled styles */
@ media ( prefers-color-scheme : dark ) {
. dog-calculator-container . theme-system . dog-calculator-percentage-slider : disabled {
background : #2a2530 ;
}
. dog-calculator-container . theme-system . dog-calculator-percentage-input : disabled {
background-color : #2a2530 ;
border-color : #3a3442 ;
color : #8a8a8a ;
}
}
2025-06-26 12:29:35 +02:00
/* Food Amount Breakdown Styling */
. dog-calculator-food-amounts-section {
margin-top : 1.5 rem ;
padding : 1 rem ;
background : var ( - - bg - secondary ) ;
border-radius : 8 px ;
border : 1 px solid var ( - - border - color ) ;
}
. dog-calculator-section-title {
margin : 0 0 1 rem 0 ;
font-size : 1 rem ;
font-weight : 600 ;
color : var ( - - text - primary ) ;
}
. dog-calculator-food-amounts-list {
display : flex ;
flex-direction : column ;
gap : 0.75 rem ;
margin-bottom : 1 rem ;
}
. dog-calculator-food-amount-item {
display : flex ;
justify-content : space-between ;
align-items : center ;
padding : 0.75 rem ;
background : var ( - - bg - primary ) ;
border-radius : 6 px ;
border : 1 px solid var ( - - border - color ) ;
}
. dog-calculator-food-amount-label {
font-weight : 500 ;
color : var ( - - text - primary ) ;
display : flex ;
align-items : center ;
gap : 0.5 rem ;
}
. dog-calculator-food-percentage {
background : var ( - - primary - color ) ;
color : white ;
padding : 0.2 rem 0.5 rem ;
border-radius : 12 px ;
font-size : 0.8 rem ;
font-weight : 500 ;
}
. dog-calculator-lock-indicator {
font-size : 0.8 rem ;
opacity : 0.7 ;
}
. dog-calculator-food-amount-value {
font-weight : 600 ;
color : var ( - - text - primary ) ;
font-size : 1 rem ;
}
2025-06-26 14:32:32 +02:00
/* Warning styles for missing energy content */
. dog-calculator-warning {
color : #e11d48 ;
font-weight : 500 ;
font-size : 1.2 rem ;
text-align : left ;
cursor : help ;
}
2025-06-26 15:55:39 +02:00
/* Inline unit selector in results */
. dog-calculator-inline-unit {
margin-left : 12 px ;
min-width : 110 px ;
padding : 4 px 8 px ;
background : var ( - - bg - primary ) ;
border : 1 px solid rgba ( 241 , 154 , 95 , 0.4 ) ;
border-radius : 6 px ;
color : var ( - - text - primary ) ;
font-size : 0.9 rem ;
font-weight : 500 ;
box-shadow : 0 2 px 4 px rgba ( 0 , 0 , 0 , 0.1 ) ;
transition : all 0.2 s ease ;
-webkit- appearance : none ;
-moz- appearance : none ;
appearance : none ;
background-image : url ( "data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236f3f6d' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e" ) ;
background-repeat : no-repeat ;
background-position : right 8 px center ;
background-size : 16 px ;
padding-right : 32 px ;
}
. dog-calculator-inline-unit : hover {
border-color : #f19a5f ;
box-shadow : 0 2 px 6 px rgba ( 241 , 154 , 95 , 0.2 ) ;
}
. dog-calculator-inline-unit : focus {
outline : none ;
border-color : #f19a5f ;
box-shadow : 0 0 0 3 px rgba ( 241 , 154 , 95 , 0.1 ) ;
}
/* Inline days input in breakdown header */
. dog-calculator-inline-days {
width : 60 px ;
padding : 2 px 6 px ;
border : 1 px solid var ( - - border - color ) ;
border-radius : 4 px ;
text-align : center ;
font-size : inherit ;
font-family : inherit ;
margin : 0 4 px ;
}
2025-06-26 16:34:38 +02:00
/* Unit selection buttons */
. dog-calculator-unit-buttons {
display : flex ;
justify-content : center ;
gap : 16 px ;
margin : 24 px auto ;
flex-wrap : wrap ;
width : fit-content ;
}
. dog-calculator-unit-btn {
padding : 8 px 14 px ;
border : 2 px solid var ( - - border - color ) ;
border-radius : 6 px ;
background : var ( - - bg - primary ) ;
color : var ( - - text - primary ) ;
font-size : 0.9 rem ;
font-weight : 500 ;
cursor : pointer ;
transition : all 0.2 s ease ;
min-width : 50 px ;
text-align : center ;
}
. dog-calculator-unit-btn : hover {
border-color : #f19a5f ;
background : rgba ( 241 , 154 , 95 , 0.1 ) ;
}
. dog-calculator-unit-btn . active {
border-color : #f19a5f ;
background : #f19a5f ;
color : white ;
}
2025-08-18 14:36:25 +02:00
. dog-calculator-unit-btn : disabled {
opacity : 0.5 ;
cursor : not-allowed ;
border-color : var ( - - border - color ) ;
background : var ( - - bg - tertiary ) ;
color : var ( - - text - secondary ) ;
}
. dog-calculator-unit-btn : disabled : hover {
border-color : var ( - - border - color ) ;
background : var ( - - bg - tertiary ) ;
transform : none ;
}
2025-06-26 17:29:25 +02:00
/* Hidden unit select for compatibility */
. dog-calculator-unit-select-hidden {
display : none ;
}
2025-06-26 15:55:39 +02:00
/* Mobile responsive adjustments for inline unit selector */
@ media ( max-width : 576px ) {
. dog-calculator-result-item {
display : flex ;
flex-direction : column ;
align-items : center ;
gap : 8 px ;
}
. dog-calculator-result-label {
width : 100 % ;
text-align : center ;
margin-bottom : 4 px ;
}
. dog-calculator-result-value {
display : inline-block ;
}
. dog-calculator-inline-unit {
display : inline-block ;
margin-left : 8 px ;
min-width : 90 px ;
vertical-align : middle ;
}
/* Center the breakdown header on mobile */
. dog-calculator-section-title {
text-align : center ;
}
/* Ensure food breakdown items stay on one line */
. dog-calculator-food-amount-item {
display : flex ;
justify-content : space-between ;
align-items : center ;
flex-wrap : nowrap ;
text-align : left ;
}
. dog-calculator-food-amount-label {
flex : 1 ;
min-width : 0 ;
white-space : nowrap ;
overflow : hidden ;
text-overflow : ellipsis ;
text-align : left ;
}
. dog-calculator-food-amount-value {
flex-shrink : 0 ;
margin-left : 8 px ;
text-align : right ;
}
}
2025-06-26 12:29:35 +02:00
. dog-calculator-total-row {
display : flex ;
justify-content : space-between ;
align-items : center ;
padding : 1 rem ;
background : var ( - - primary - color ) ;
color : white ;
border-radius : 6 px ;
font-weight : 600 ;
margin-top : 0.5 rem ;
}
. dog-calculator-total-label {
font-size : 1 rem ;
}
. dog-calculator-total-value {
font-size : 1.1 rem ;
font-weight : 700 ;
}
. dog-calculator-full-width {
flex : 1 ;
}
2025-06-26 12:34:06 +02:00
/* Editable Food Source Name Styling */
. dog-calculator-food-source-name-input {
background : transparent ;
border : 2 px solid transparent ;
color : var ( - - text - primary ) ;
font-size : 1.1 rem ;
font-weight : 600 ;
font-family : inherit ;
2025-06-26 13:02:28 +02:00
padding : 0.5 rem 0 ;
2025-06-26 12:34:06 +02:00
border-radius : 4 px ;
width : 100 % ;
outline : none ;
transition : all 0.2 s ease ;
cursor : text ;
}
. dog-calculator-food-source-name-input : hover {
border-color : var ( - - border - color ) ;
background : var ( - - bg - secondary ) ;
2025-06-26 13:02:28 +02:00
padding : 0.5 rem ;
2025-06-26 12:34:06 +02:00
}
. dog-calculator-food-source-name-input : focus {
border-color : var ( - - primary - color ) ;
background : var ( - - bg - primary ) ;
box-shadow : 0 0 0 3 px rgba ( 241 , 154 , 95 , 0.1 ) ;
2025-06-26 13:02:28 +02:00
padding : 0.5 rem ;
2025-06-26 12:34:06 +02:00
}
. dog-calculator-food-source-name-input :: placeholder {
color : var ( - - text - secondary ) ;
opacity : 0.7 ;
}
/* Dark theme adjustments */
. dog-calculator-container . theme-dark . dog-calculator-food-source-name-input : hover {
background : #2a2530 ;
}
. dog-calculator-container . theme-dark . dog-calculator-food-source-name-input : focus {
background : #1e1a24 ;
}
/* System theme adjustments */
@ media ( prefers-color-scheme : dark ) {
. dog-calculator-container . theme-system . dog-calculator-food-source-name-input : hover {
background : #2a2530 ;
}
. dog-calculator-container . theme-system . dog-calculator-food-source-name-input : focus {
background : #1e1a24 ;
}
}
2025-06-26 12:29:35 +02:00
/* Responsive adjustments */
@ media ( max-width : 576px ) {
. dog-calculator-food-amount-item {
2025-06-26 15:55:39 +02:00
flex-direction : row !important ;
2025-06-26 12:29:35 +02:00
gap : 0.5 rem ;
2025-06-26 15:55:39 +02:00
text-align : left !important ;
justify-content : space-between !important ;
align-items : center !important ;
flex-wrap : nowrap !important ;
2025-06-26 12:29:35 +02:00
}
. dog-calculator-food-amount-label {
2025-06-26 15:55:39 +02:00
justify-content : flex-start !important ;
text-align : left !important ;
flex : 1 ;
min-width : 0 ;
white-space : nowrap ;
overflow : hidden ;
text-overflow : ellipsis ;
}
. dog-calculator-food-amount-value {
flex-shrink : 0 ;
margin-left : 8 px ;
text-align : right !important ;
2025-06-26 12:29:35 +02:00
}
2025-06-26 12:34:06 +02:00
. dog-calculator-food-source-name-input {
font-size : 1 rem ;
}
2025-06-26 13:02:28 +02:00
2025-06-26 12:29:35 +02:00
}
2025-06-08 20:31:11 +02:00
< / style >
< / head >
< body >
< div class = "dog-calculator-container" id = "dogCalculator" >
< div class = "dog-calculator-section" >
2025-06-08 22:27:21 +02:00
< div class = "dog-calculator-section-header" >
2025-11-12 16:44:43 +01:00
< h2 > Kaya’ s Transition< / h2 >
2025-06-08 20:31:11 +02:00
< / div >
< div class = "dog-calculator-form-group" >
2025-11-12 16:44:43 +01:00
< label for = "ageMonths" > Kaya’ s age (months):< / label >
< input type = "number" id = "ageMonths" min = "2" max = "12" step = "0.1" placeholder = "Enter age in months" aria-describedby = "ageHelp" >
< div id = "ageClampNote" class = "dog-calculator-error dog-calculator-hidden" > Age adjusted to the supported 2– 12 month range.< / div >
2025-06-08 20:31:11 +02:00
< / div >
2026-01-28 15:51:28 +01:00
< div class = "dog-calculator-form-group" >
< label for = "weight" id = "weightLabel" > Kaya’ s current weight (kg):< / label >
< input type = "number" id = "weight" min = "0.1" step = "0.1" placeholder = "Enter current weight in kg" aria-describedby = "weightHelp" >
< div id = "weightError" class = "dog-calculator-error dog-calculator-hidden" > Please enter a valid weight (minimum 0.1 kg)< / div >
< / div >
2025-11-12 17:00:45 +01:00
2025-11-12 16:44:43 +01:00
< div class = "dog-calculator-form-group" >
< label for = "kayaEndWeight" > Kaya’ s end‑ weight:< / label >
< input type = "text" id = "kayaEndWeight" value = "30 kg" readonly >
2025-06-08 20:31:11 +02:00
< / div >
< / div >
2025-06-08 22:27:21 +02:00
< div class = "dog-calculator-collapsible active" id = "foodCalculator" >
< div class = "dog-calculator-collapsible-header" >
< h3 > How much should I feed?< / h3 >
2025-06-08 20:31:11 +02:00
< / div >
< div class = "dog-calculator-collapsible-content" >
< div class = "dog-calculator-collapsible-inner" >
2025-06-26 11:09:29 +02:00
<!-- Food Sources Container -->
< div class = "dog-calculator-food-sources" id = "foodSources" >
<!-- Initial food source will be added by JavaScript -->
< / div >
<!-- Add Food Source Button -->
< button class = "dog-calculator-add-food-btn" id = "addFoodBtn" type = "button" >
< span > +< / span >
2025-06-26 13:02:28 +02:00
< span > Add another food source< / span >
2025-06-26 11:09:29 +02:00
< / button >
2025-08-18 12:45:44 +02:00
<!-- Feeding Configuration -->
< div class = "dog-calculator-feeding-config" id = "feedingConfig" style = "display: none;" >
< div class = "dog-calculator-frequency-row" >
< label > Show amounts:< / label >
< div class = "dog-calculator-radio-group" >
< label >
< input type = "radio" name = "showAs" value = "daily" id = "showDaily" checked >
< span > Per day< / span >
< / label >
< label >
< input type = "radio" name = "showAs" value = "meal" id = "showPerMeal" >
< span > Per meal< / span >
< / label >
< / div >
< div class = "dog-calculator-meal-input" id = "mealInputGroup" style = "display: none;" >
< span > × < / span >
< input type = "number" id = "mealsPerDay" value = "2" min = "1" max = "10" >
< span > meals/day< / span >
< / div >
< / div >
< / div >
2025-06-26 11:09:29 +02:00
<!-- Per - Food Results -->
< div class = "dog-calculator-food-results" id = "foodBreakdownResults" style = "display: none;" >
< div id = "foodBreakdownList" >
<!-- Individual food breakdowns will be populated by JavaScript -->
2025-06-09 09:53:16 +02:00
< / div >
2025-06-08 20:31:11 +02:00
< / div >
2025-06-26 16:34:38 +02:00
<!-- Unit Selection Buttons -->
< div class = "dog-calculator-unit-buttons" id = "unitButtons" style = "display: none;" >
< button type = "button" class = "dog-calculator-unit-btn active" data-unit = "g" > g< / button >
< button type = "button" class = "dog-calculator-unit-btn" data-unit = "kg" > kg< / button >
< / div >
2025-06-26 11:09:29 +02:00
<!-- Daily Total Results -->
2025-06-08 20:31:11 +02:00
< div class = "dog-calculator-results" id = "dailyFoodResults" style = "display: none;" >
< div class = "dog-calculator-result-item" >
2025-06-26 11:09:29 +02:00
< span class = "dog-calculator-result-label" > Total Daily Amount:< / span >
2025-06-08 20:31:11 +02:00
< span class = "dog-calculator-result-value" id = "dailyFoodValue" > - g/day< / span >
< / div >
< / div >
2025-06-26 12:29:35 +02:00
2025-06-26 16:34:38 +02:00
<!-- Hidden select for compatibility -->
2025-06-26 17:29:25 +02:00
< select id = "unit" class = "dog-calculator-unit-select-hidden" aria-describedby = "unitHelp" >
2025-06-26 16:34:38 +02:00
< option value = "g" > grams (g)< / option >
< option value = "kg" > kilograms (kg)< / option >
< / select >
2025-06-26 12:29:35 +02:00
< div class = "dog-calculator-food-amounts-section" id = "foodAmountsSection" style = "display: none;" >
2025-06-26 15:55:39 +02:00
< h4 class = "dog-calculator-section-title" >
Calculate amounts for
< input type = "number" id = "days" min = "1" step = "1" value = "1" placeholder = "1" aria-describedby = "daysHelp" class = "dog-calculator-inline-days" >
2025-08-18 12:45:44 +02:00
< span id = "dayLabel" > day< / span > < span id = "mealNote" style = "display: none;" > < / span > :
2025-06-26 15:55:39 +02:00
< / h4 >
< div id = "daysError" class = "dog-calculator-error dog-calculator-hidden" > Please enter a valid number of days (minimum 1)< / div >
2025-06-26 12:29:35 +02:00
< div id = "foodAmountsList" class = "dog-calculator-food-amounts-list" >
<!-- Individual food amounts will be populated here -->
< / div >
< div class = "dog-calculator-total-row" id = "totalAmountRow" >
< span class = "dog-calculator-total-label" > Total Amount:< / span >
< span class = "dog-calculator-total-value" id = "totalAmountDisplay" > < / span >
< / div >
< / div >
2025-06-08 20:31:11 +02:00
< / div >
< / div >
< / div >
< div class = "dog-calculator-footer" >
< a href = "https://caninenutritionandwellness.com" target = "_blank" rel = "noopener noreferrer" >
by caninenutritionandwellness.com
< / a >
< / div >
2025-06-08 23:45:32 +02:00
< / div >
2025-06-08 20:31:11 +02:00
< script >
2025-06-08 22:27:21 +02:00
/**
2025-08-18 09:05:00 +02:00
* Configuration constants for Dog Calorie Calculator
*/
const CALCULATOR _CONFIG = {
defaultTheme : 'system' ,
defaultScale : 1.0 ,
maxFoodSources : 5 ,
minScale : 0.5 ,
2026-01-28 15:51:28 +01:00
maxScale : 2.0 ,
// Kaya fork: Fred & Felia uses MER; kibble stays chart-based (30 kg column).
// Used only when weight input is not present (back-compat).
kayaMerDefaultWeightKg : 30 ,
kayaMerFactorUnder4Months : 3.0 ,
kayaMerFactorFrom4Months : 2.0
2025-08-18 09:05:00 +02:00
} ;
/**
2025-06-08 22:27:21 +02:00
* Dog Calorie Calculator - iframe version
* by Canine Nutrition and Wellness
*/
class DogCalorieCalculator {
constructor ( ) {
this . currentMER = 0 ;
2025-08-18 15:33:44 +02:00
this . currentMERMin = 0 ; // For range calculations
this . currentMERMax = 0 ; // For range calculations
2025-06-08 22:27:21 +02:00
this . isImperial = false ;
2025-08-18 09:05:00 +02:00
this . theme = this . getThemeFromURL ( ) || CALCULATOR _CONFIG . defaultTheme ;
this . scale = this . getScaleFromURL ( ) || CALCULATOR _CONFIG . defaultScale ;
2025-06-26 11:09:29 +02:00
this . foodSources = [ ] ;
2025-08-18 09:05:00 +02:00
this . maxFoodSources = CALCULATOR _CONFIG . maxFoodSources ;
2025-08-18 12:45:44 +02:00
this . mealsPerDay = 2 ;
this . showPerMeal = false ;
2025-11-12 16:44:43 +01:00
// Kayafied reference source tracking
this . kibbleRefId = null ;
2026-01-28 15:51:28 +01:00
this . fredRefId = null ;
2025-11-12 18:39:10 +01:00
this . storageKey = 'kaya_calculator_state_v1' ;
2025-06-08 22:27:21 +02:00
this . init ( ) ;
}
init ( ) {
this . applyTheme ( ) ;
2025-06-09 10:01:29 +02:00
this . applyScale ( ) ;
2025-06-08 22:27:21 +02:00
this . updateUnitLabels ( ) ;
2025-11-12 18:39:10 +01:00
const restored = this . loadStateFromStorage ( ) ;
if ( ! restored ) {
this . initializeFoodSources ( ) ;
}
this . bindEvents ( ) ;
2025-06-08 22:27:21 +02:00
this . setupIframeResize ( ) ;
// Show the calculator with fade-in
const container = document . getElementById ( 'dogCalculator' ) ;
container . classList . add ( 'loaded' ) ;
}
2025-11-12 18:39:10 +01:00
// Persistence helpers
saveStateToStorage ( ) {
try {
const ageInput = document . getElementById ( 'ageMonths' ) ;
2026-01-28 15:51:28 +01:00
const weightInput = document . getElementById ( 'weight' ) ;
2025-11-12 18:39:10 +01:00
const unitSelect = document . getElementById ( 'unit' ) ;
const daysInput = document . getElementById ( 'days' ) ;
const state = {
version : 1 ,
age : ageInput && ageInput . value !== '' ? parseFloat ( ageInput . value ) : null ,
2026-01-28 15:51:28 +01:00
weight : weightInput && weightInput . value !== '' ? parseFloat ( weightInput . value ) : null ,
2025-11-12 18:39:10 +01:00
unit : unitSelect ? unitSelect . value : 'g' ,
days : daysInput && daysInput . value ? parseInt ( daysInput . value ) : 1 ,
showPerMeal : ! ! this . showPerMeal ,
mealsPerDay : this . mealsPerDay ,
foodSources : this . foodSources . map ( fs => ( {
id : fs . id ,
name : fs . name ,
energy : fs . energy ,
energyUnit : fs . energyUnit ,
percentage : fs . percentage ,
isLocked : fs . isLocked ,
chartType : fs . chartType || null ,
splitByMeals : ( fs . splitByMeals === undefined ? true : fs . splitByMeals )
} ) )
} ;
localStorage . setItem ( this . storageKey , JSON . stringify ( state ) ) ;
} catch ( e ) {
// Ignore storage errors (private mode, etc.)
}
}
loadStateFromStorage ( ) {
try {
const raw = localStorage . getItem ( this . storageKey ) ;
if ( ! raw ) return false ;
const state = JSON . parse ( raw ) ;
if ( ! state || typeof state !== 'object' ) return false ;
// Restore unit first
const unitSelect = document . getElementById ( 'unit' ) ;
if ( unitSelect && state . unit && ( state . unit === 'g' || state . unit === 'kg' ) ) {
unitSelect . value = state . unit ;
this . setActiveUnitButton ( state . unit ) ;
}
// Restore days
const daysInput = document . getElementById ( 'days' ) ;
if ( daysInput && state . days ) {
daysInput . value = state . days ;
this . updateDayLabel ( ) ;
}
// Restore meal settings
this . showPerMeal = ! ! state . showPerMeal ;
this . mealsPerDay = state . mealsPerDay || 2 ;
const showDaily = document . getElementById ( 'showDaily' ) ;
const showPerMeal = document . getElementById ( 'showPerMeal' ) ;
const mealsPerDayInput = document . getElementById ( 'mealsPerDay' ) ;
const mealInputGroup = document . getElementById ( 'mealInputGroup' ) ;
if ( showDaily && showPerMeal ) {
if ( this . showPerMeal ) {
showPerMeal . checked = true ;
if ( mealInputGroup ) mealInputGroup . style . display = 'inline-flex' ;
} else {
showDaily . checked = true ;
if ( mealInputGroup ) mealInputGroup . style . display = 'none' ;
}
}
if ( mealsPerDayInput ) {
mealsPerDayInput . value = this . mealsPerDay ;
}
// Restore age
const ageInput = document . getElementById ( 'ageMonths' ) ;
if ( ageInput && ( state . age || state . age === 0 ) ) {
ageInput . value = state . age ;
}
2026-01-28 15:51:28 +01:00
// Restore weight (used for Fred & Felia MER calculation)
const weightInput = document . getElementById ( 'weight' ) ;
if ( weightInput && ( state . weight || state . weight === 0 ) ) {
weightInput . value = state . weight ;
}
2025-11-12 18:39:10 +01:00
// Restore food sources
if ( Array . isArray ( state . foodSources ) && state . foodSources . length ) {
this . foodSources = [ ] ;
this . kibbleRefId = null ;
2026-01-28 15:51:28 +01:00
this . fredRefId = null ;
2025-11-12 18:39:10 +01:00
state . foodSources . forEach ( saved => {
2026-01-28 15:51:28 +01:00
// Back-compat: older Kaya state stored Fred & Felia as `chartType: "gc"`.
const chartType = saved . chartType === 'gc' ? 'mer' : ( saved . chartType || null ) ;
2026-02-06 12:52:13 +01:00
const normalizedEnergy = chartType === 'mer' ? '115' : ( saved . energy || '' ) ;
const normalizedEnergyUnit = chartType === 'mer' ? 'kcal100g' : ( saved . energyUnit || 'kcal100g' ) ;
2025-11-12 18:39:10 +01:00
const fs = {
id : saved . id || this . generateFoodSourceId ( ) ,
name : saved . name || 'Food Source' ,
2026-02-06 12:52:13 +01:00
energy : normalizedEnergy ,
energyUnit : normalizedEnergyUnit ,
2025-11-12 18:39:10 +01:00
percentage : typeof saved . percentage === 'number' ? saved . percentage : 0 ,
isLocked : ! ! saved . isLocked ,
2026-01-28 15:51:28 +01:00
chartType : chartType ,
2025-11-12 18:39:10 +01:00
splitByMeals : ( saved . splitByMeals === undefined ? true : saved . splitByMeals )
} ;
this . foodSources . push ( fs ) ;
this . renderFoodSource ( fs ) ;
if ( fs . chartType === 'kibble' && ! this . kibbleRefId ) this . kibbleRefId = fs . id ;
2026-01-28 15:51:28 +01:00
if ( fs . chartType === 'mer' && ! this . fredRefId ) this . fredRefId = fs . id ;
2025-11-12 18:39:10 +01:00
} ) ;
this . updateAddButton ( ) ;
this . updateRemoveButtons ( ) ;
this . refreshAllPercentageUI ( ) ;
}
// Trigger calculations
this . updateCalorieCalculations ( ) ;
return true ;
} catch ( e ) {
return false ;
}
}
2025-06-08 22:27:21 +02:00
getThemeFromURL ( ) {
const urlParams = new URLSearchParams ( window . location . search ) ;
const theme = urlParams . get ( 'theme' ) ;
return [ 'light' , 'dark' , 'system' ] . includes ( theme ) ? theme : null ;
}
2025-06-09 10:01:29 +02:00
getScaleFromURL ( ) {
const urlParams = new URLSearchParams ( window . location . search ) ;
const scale = parseFloat ( urlParams . get ( 'scale' ) ) ;
2025-08-18 09:05:00 +02:00
return ( ! isNaN ( scale ) && scale >= CALCULATOR _CONFIG . minScale && scale <= CALCULATOR _CONFIG . maxScale ) ? scale : null ;
2025-06-09 10:01:29 +02:00
}
2025-06-08 22:27:21 +02:00
applyTheme ( ) {
const container = document . getElementById ( 'dogCalculator' ) ;
container . classList . remove ( 'theme-light' , 'theme-dark' , 'theme-system' ) ;
2025-06-08 23:45:32 +02:00
container . classList . add ( 'theme-' + this . theme ) ;
2025-06-08 22:27:21 +02:00
}
2025-06-09 10:01:29 +02:00
applyScale ( ) {
const container = document . getElementById ( 'dogCalculator' ) ;
if ( ! container ) return ;
2025-08-18 09:05:00 +02:00
// Clamp scale between min and max for usability
const clampedScale = Math . max ( CALCULATOR _CONFIG . minScale , Math . min ( CALCULATOR _CONFIG . maxScale , this . scale ) ) ;
2025-06-09 10:01:29 +02:00
if ( clampedScale !== 1.0 ) {
container . style . transform = ` scale( ${ clampedScale } ) ` ;
container . style . transformOrigin = 'top center' ;
2025-10-28 09:58:20 +01:00
// Recalculate height for parent without adding artificial margins
2025-06-09 10:01:29 +02:00
setTimeout ( ( ) => {
this . sendHeightToParent ( ) ;
} , 100 ) ;
}
}
2025-06-26 11:09:29 +02:00
// Food Source Management Methods
initializeFoodSources ( ) {
2025-11-12 16:44:43 +01:00
// Seed three sources for Kaya's transition
2026-01-28 15:51:28 +01:00
const fred = {
2025-11-12 16:44:43 +01:00
id : this . generateFoodSourceId ( ) ,
2025-11-12 18:00:36 +01:00
name : 'Fred & Felia (Junior Huhn)' ,
2025-11-12 16:44:43 +01:00
energy : '115' ,
energyUnit : 'kcal100g' ,
percentage : 5 ,
2025-11-12 18:00:36 +01:00
isLocked : false ,
2026-01-28 15:51:28 +01:00
chartType : 'mer'
2025-11-12 16:44:43 +01:00
} ;
2026-01-28 15:51:28 +01:00
this . foodSources . push ( fred ) ;
this . renderFoodSource ( fred ) ;
this . fredRefId = fred . id ;
2025-11-12 16:44:43 +01:00
const kibble = {
id : this . generateFoodSourceId ( ) ,
2025-11-12 18:00:36 +01:00
name : 'Eukanuba (Large Breed Fresh Chicken)' ,
2025-11-12 16:44:43 +01:00
energy : '372' ,
energyUnit : 'kcal100g' ,
percentage : 95 ,
2025-11-12 18:00:36 +01:00
isLocked : false ,
chartType : 'kibble'
2025-11-12 16:44:43 +01:00
} ;
this . foodSources . push ( kibble ) ;
this . renderFoodSource ( kibble ) ;
this . kibbleRefId = kibble . id ;
const treats = {
id : this . generateFoodSourceId ( ) ,
name : 'Treats' ,
energy : '' ,
energyUnit : 'kcal100g' ,
percentage : 0 ,
2025-11-12 18:00:36 +01:00
isLocked : false ,
2025-11-12 18:34:26 +01:00
chartType : null ,
splitByMeals : false
2025-11-12 16:44:43 +01:00
} ;
this . foodSources . push ( treats ) ;
this . renderFoodSource ( treats ) ;
2025-06-26 11:09:29 +02:00
this . updateAddButton ( ) ;
2025-11-12 16:44:43 +01:00
this . updateRemoveButtons ( ) ;
this . refreshAllPercentageUI ( ) ;
2025-06-26 11:09:29 +02:00
}
addFoodSource ( ) {
if ( this . foodSources . length >= this . maxFoodSources ) {
return ;
}
const id = this . generateFoodSourceId ( ) ;
const foodSource = {
id : id ,
name : ` Food Source ${ this . foodSources . length + 1 } ` ,
energy : '' ,
energyUnit : this . isImperial ? 'kcalcup' : 'kcal100g' ,
2025-06-26 11:19:29 +02:00
percentage : this . foodSources . length === 0 ? 100 : 0 ,
isLocked : false
2025-06-26 11:09:29 +02:00
} ;
this . foodSources . push ( foodSource ) ;
this . redistributePercentages ( ) ;
this . renderFoodSource ( foodSource ) ;
this . updateAddButton ( ) ;
2025-06-26 14:32:32 +02:00
this . updateRemoveButtons ( ) ;
2025-06-26 12:21:18 +02:00
this . refreshAllPercentageUI ( ) ;
2025-11-12 18:39:10 +01:00
this . saveStateToStorage ( ) ;
2025-06-26 11:09:29 +02:00
}
removeFoodSource ( id ) {
if ( this . foodSources . length <= 1 ) {
return ; // Cannot remove the last food source
}
const index = this . foodSources . findIndex ( fs => fs . id === id ) ;
if ( index === - 1 ) return ;
this . foodSources . splice ( index , 1 ) ;
2025-11-12 16:44:43 +01:00
// If reference IDs were removed, clear them
if ( this . kibbleRefId === id ) this . kibbleRefId = null ;
2026-01-28 15:51:28 +01:00
if ( this . fredRefId === id ) this . fredRefId = null ;
2025-06-26 11:09:29 +02:00
// Remove the DOM element
const element = document . getElementById ( ` foodSource- ${ id } ` ) ;
if ( element ) {
element . remove ( ) ;
}
// Redistribute percentages among remaining sources
this . redistributePercentages ( ) ;
this . updateFoodSourceNames ( ) ;
this . updateAddButton ( ) ;
2025-06-26 14:32:32 +02:00
this . updateRemoveButtons ( ) ;
2025-06-26 12:21:18 +02:00
this . refreshAllPercentageUI ( ) ;
2025-11-12 16:44:43 +01:00
this . updateCalorieCalculations ( ) ;
2025-11-12 18:39:10 +01:00
this . saveStateToStorage ( ) ;
2025-06-26 11:09:29 +02:00
}
generateFoodSourceId ( ) {
return 'fs_' + Date . now ( ) + '_' + Math . random ( ) . toString ( 36 ) . substr ( 2 , 5 ) ;
}
redistributePercentages ( ) {
const count = this . foodSources . length ;
if ( count === 0 ) return ;
2025-06-26 11:19:29 +02:00
// Only redistribute among unlocked sources
const unlockedSources = this . foodSources . filter ( fs => ! fs . isLocked ) ;
const lockedSources = this . foodSources . filter ( fs => fs . isLocked ) ;
// Calculate total locked percentage
const totalLockedPercentage = lockedSources . reduce ( ( sum , fs ) => sum + fs . percentage , 0 ) ;
// Available percentage for unlocked sources
const availablePercentage = 100 - totalLockedPercentage ;
if ( unlockedSources . length > 0 ) {
const equalPercentage = Math . floor ( availablePercentage / unlockedSources . length ) ;
const remainder = availablePercentage - ( equalPercentage * unlockedSources . length ) ;
2025-06-26 11:09:29 +02:00
2025-06-26 11:19:29 +02:00
unlockedSources . forEach ( ( fs , index ) => {
fs . percentage = equalPercentage + ( index < remainder ? 1 : 0 ) ;
} ) ;
}
2025-06-26 11:09:29 +02:00
// Update the UI sliders and inputs
2025-06-26 12:21:18 +02:00
this . refreshAllPercentageUI ( ) ;
2025-06-26 11:09:29 +02:00
}
2025-06-26 12:21:18 +02:00
// OBSOLETE METHODS - Replaced by new validation system
// Keeping for reference but these are no longer used
/*
2025-06-26 11:09:29 +02:00
updatePercentageInputs() {
this.foodSources.forEach(fs => {
const slider = document.getElementById(`percentage-slider-${fs.id}`);
const input = document.getElementById(`percentage-input-${fs.id}`);
2025-06-26 11:27:03 +02:00
const display = document.getElementById(`percentage-display-${fs.id}`);
2025-06-26 11:09:29 +02:00
if (slider) slider.value = fs.percentage;
if (input) input.value = fs.percentage;
2025-06-26 11:27:03 +02:00
if (display) display.textContent = `${fs.percentage}%`;
});
// Update constraints after values are set
this.updatePercentageConstraints();
}
updatePercentageConstraints() {
this.foodSources.forEach(fs => {
const slider = document.getElementById(`percentage-slider-${fs.id}`);
const input = document.getElementById(`percentage-input-${fs.id}`);
if (!slider || !input) return;
2025-06-26 11:47:33 +02:00
// Always keep full 0-100 scale for all sliders
slider.max = 100;
input.max = 100;
2025-06-26 11:27:03 +02:00
if (fs.isLocked) {
// Locked sources can't be changed
slider.disabled = true;
input.disabled = true;
} else {
// Calculate the maximum this source can have
const lockedSources = this.foodSources.filter(other => other.id !== fs.id && other.isLocked);
const totalLockedPercentage = lockedSources.reduce((sum, other) => sum + other.percentage, 0);
const maxAllowed = 100 - totalLockedPercentage;
2025-06-26 11:47:33 +02:00
// Re-enable
2025-06-26 11:27:03 +02:00
slider.disabled = false;
input.disabled = false;
2025-06-26 11:47:33 +02:00
// Store max allowed for validation (we'll check this in event handlers)
slider.dataset.maxAllowed = maxAllowed;
input.dataset.maxAllowed = maxAllowed;
2025-06-26 11:27:03 +02:00
// If current value exceeds max, adjust it
if (fs.percentage > maxAllowed) {
fs.percentage = maxAllowed;
slider.value = maxAllowed;
input.value = maxAllowed;
document.getElementById(`percentage-display-${fs.id}`).textContent = `${maxAllowed}%`;
}
}
2025-06-26 11:09:29 +02:00
});
}
adjustPercentages(changedId, newPercentage) {
const changedIndex = this.foodSources.findIndex(fs => fs.id === changedId);
if (changedIndex === -1) return;
const oldPercentage = this.foodSources[changedIndex].percentage;
const difference = newPercentage - oldPercentage;
this.foodSources[changedIndex].percentage = newPercentage;
2025-06-26 11:19:29 +02:00
// Only redistribute among unlocked sources (excluding the changed one)
const otherUnlockedSources = this.foodSources.filter((fs, index) =>
index !== changedIndex && !fs.isLocked
);
2025-06-26 11:47:33 +02:00
// If this is the only unlocked source, force it to fill remaining percentage
if (otherUnlockedSources.length === 0) {
const lockedSources = this.foodSources.filter((fs, index) =>
index !== changedIndex && fs.isLocked
);
const totalLockedPercentage = lockedSources.reduce((sum, fs) => sum + fs.percentage, 0);
const requiredPercentage = 100 - totalLockedPercentage;
// Force the changed source to the required percentage
this.foodSources[changedIndex].percentage = requiredPercentage;
this.updatePercentageInputs();
this.updateFoodCalculations();
return;
}
2025-06-26 11:09:29 +02:00
2025-06-26 11:19:29 +02:00
// Calculate total locked percentage (excluding the changed source)
const lockedSources = this.foodSources.filter((fs, index) =>
index !== changedIndex && fs.isLocked
);
const totalLockedPercentage = lockedSources.reduce((sum, fs) => sum + fs.percentage, 0);
2025-06-26 11:09:29 +02:00
2025-06-26 11:19:29 +02:00
// Available percentage for unlocked sources
const availablePercentage = 100 - newPercentage - totalLockedPercentage;
const totalUnlockedPercentage = otherUnlockedSources.reduce((sum, fs) => sum + fs.percentage, 0);
if (totalUnlockedPercentage === 0) {
// If all other unlocked sources are 0, distribute equally
const equalShare = Math.floor(availablePercentage / otherUnlockedSources.length);
const remainder = availablePercentage - (equalShare * otherUnlockedSources.length);
2025-06-26 11:09:29 +02:00
2025-06-26 11:19:29 +02:00
otherUnlockedSources.forEach((fs, index) => {
2025-06-26 11:09:29 +02:00
fs.percentage = equalShare + (index < remainder ? 1 : 0);
});
} else {
2025-06-26 11:19:29 +02:00
// Distribute proportionally among unlocked sources
const scale = availablePercentage / totalUnlockedPercentage;
2025-06-26 11:09:29 +02:00
let distributedTotal = 0;
2025-06-26 11:19:29 +02:00
otherUnlockedSources.forEach((fs, index) => {
if (index === otherUnlockedSources.length - 1) {
2025-06-26 11:09:29 +02:00
// Last item gets the remainder to ensure exact 100%
2025-06-26 11:19:29 +02:00
fs.percentage = availablePercentage - distributedTotal;
2025-06-26 11:09:29 +02:00
} else {
fs.percentage = Math.round(fs.percentage * scale);
distributedTotal += fs.percentage;
}
});
}
this.updatePercentageInputs();
this.updateFoodCalculations();
}
2025-06-26 12:21:18 +02:00
*/
// New validation system methods
validatePercentageChange ( sourceId , requestedValue ) {
// Find the source being changed
const changedSource = this . foodSources . find ( fs => fs . id === sourceId ) ;
if ( ! changedSource ) {
return { isValid : false , reason : 'Source not found' } ;
}
// If the source is locked, no change allowed
if ( changedSource . isLocked ) {
return { isValid : false , reason : 'Source is locked' } ;
}
// Ensure requested value is within bounds
const clampedValue = Math . max ( 0 , Math . min ( 100 , requestedValue ) ) ;
// Calculate locked and other unlocked totals
const lockedSources = this . foodSources . filter ( fs => fs . id !== sourceId && fs . isLocked ) ;
const otherUnlockedSources = this . foodSources . filter ( fs => fs . id !== sourceId && ! fs . isLocked ) ;
const totalLocked = lockedSources . reduce ( ( sum , fs ) => sum + fs . percentage , 0 ) ;
// Check if the only unlocked source
if ( otherUnlockedSources . length === 0 ) {
// This is the only unlocked source, must fill remaining percentage
const requiredPercentage = 100 - totalLocked ;
return {
isValid : true ,
actualValue : requiredPercentage ,
affectedSources : [ { id : sourceId , newPercentage : requiredPercentage } ] ,
reason : 'Only unlocked source, forced to fill remainder'
} ;
}
// Calculate available percentage for redistribution
const availableForOthers = 100 - clampedValue - totalLocked ;
// Check if redistribution is possible
if ( availableForOthers < 0 ) {
// Cannot accommodate this value
const maxAllowed = 100 - totalLocked ;
return {
isValid : true ,
actualValue : maxAllowed ,
affectedSources : this . calculateRedistribution ( sourceId , maxAllowed , otherUnlockedSources ) ,
reason : 'Value clamped to maximum allowed'
} ;
}
// Calculate redistribution
const affectedSources = this . calculateRedistribution ( sourceId , clampedValue , otherUnlockedSources ) ;
return {
isValid : true ,
actualValue : clampedValue ,
affectedSources : affectedSources ,
reason : 'Valid change'
} ;
}
calculateRedistribution ( sourceId , newValue , otherUnlockedSources ) {
const result = [ { id : sourceId , newPercentage : newValue } ] ;
if ( otherUnlockedSources . length === 0 ) {
return result ;
}
// Calculate total locked percentage
const lockedSources = this . foodSources . filter ( fs => fs . id !== sourceId && fs . isLocked ) ;
const totalLocked = lockedSources . reduce ( ( sum , fs ) => sum + fs . percentage , 0 ) ;
// Available percentage for other unlocked sources
const availableForOthers = 100 - newValue - totalLocked ;
// Current total of other unlocked sources
const currentOtherTotal = otherUnlockedSources . reduce ( ( sum , fs ) => sum + fs . percentage , 0 ) ;
if ( currentOtherTotal === 0 || availableForOthers === 0 ) {
// Distribute equally among other unlocked sources
const equalShare = Math . floor ( availableForOthers / otherUnlockedSources . length ) ;
const remainder = availableForOthers - ( equalShare * otherUnlockedSources . length ) ;
otherUnlockedSources . forEach ( ( fs , index ) => {
const newPercentage = equalShare + ( index < remainder ? 1 : 0 ) ;
result . push ( { id : fs . id , newPercentage } ) ;
} ) ;
} else {
// Distribute proportionally
const scale = availableForOthers / currentOtherTotal ;
let distributedTotal = 0 ;
otherUnlockedSources . forEach ( ( fs , index ) => {
let newPercentage ;
if ( index === otherUnlockedSources . length - 1 ) {
// Last item gets remainder to ensure exact total
newPercentage = availableForOthers - distributedTotal ;
} else {
newPercentage = Math . round ( fs . percentage * scale ) ;
distributedTotal += newPercentage ;
}
result . push ( { id : fs . id , newPercentage } ) ;
} ) ;
}
return result ;
}
applyValidatedChanges ( validationResult ) {
if ( ! validationResult . isValid ) {
return false ;
}
// Apply all percentage changes
validationResult . affectedSources . forEach ( change => {
const source = this . foodSources . find ( fs => fs . id === change . id ) ;
if ( source ) {
source . percentage = change . newPercentage ;
}
} ) ;
2025-11-12 18:39:10 +01:00
this . saveStateToStorage ( ) ;
2025-06-26 12:21:18 +02:00
return true ;
}
refreshAllPercentageUI ( ) {
this . foodSources . forEach ( fs => {
// Update all UI elements from single source of truth
const slider = document . getElementById ( ` percentage-slider- ${ fs . id } ` ) ;
const input = document . getElementById ( ` percentage-input- ${ fs . id } ` ) ;
const display = document . getElementById ( ` percentage-display- ${ fs . id } ` ) ;
if ( slider ) slider . value = fs . percentage ;
if ( input ) input . value = fs . percentage ;
if ( display ) display . textContent = ` ${ fs . percentage } % ` ;
// Update constraints and disabled states
this . updateSliderConstraints ( fs ) ;
} ) ;
// Update food calculations
this . updateFoodCalculations ( ) ;
2025-11-12 18:39:10 +01:00
this . saveStateToStorage ( ) ;
2025-06-26 12:21:18 +02:00
}
updateSliderConstraints ( foodSource ) {
const slider = document . getElementById ( ` percentage-slider- ${ foodSource . id } ` ) ;
const input = document . getElementById ( ` percentage-input- ${ foodSource . id } ` ) ;
if ( ! slider || ! input ) return ;
// Always keep 0-100 scale
slider . max = 100 ;
input . max = 100 ;
if ( foodSource . isLocked ) {
slider . disabled = true ;
input . disabled = true ;
} else {
// Calculate maximum allowed and store for validation
const maxAllowed = this . calculateMaxAllowed ( foodSource . id ) ;
slider . disabled = ( maxAllowed <= 0 ) ;
input . disabled = ( maxAllowed <= 0 ) ;
slider . dataset . maxAllowed = maxAllowed ;
input . dataset . maxAllowed = maxAllowed ;
}
}
calculateMaxAllowed ( sourceId ) {
const lockedSources = this . foodSources . filter ( fs => fs . id !== sourceId && fs . isLocked ) ;
const otherUnlockedSources = this . foodSources . filter ( fs => fs . id !== sourceId && ! fs . isLocked ) ;
const totalLocked = lockedSources . reduce ( ( sum , fs ) => sum + fs . percentage , 0 ) ;
// If this is the only unlocked source, it must take up the remainder
if ( otherUnlockedSources . length === 0 ) {
return 100 - totalLocked ;
}
// Otherwise, maximum is 100 minus locked percentages
return Math . max ( 0 , 100 - totalLocked ) ;
}
2025-06-26 11:09:29 +02:00
updateFoodSourceNames ( ) {
this . foodSources . forEach ( ( fs , index ) => {
2025-06-26 12:34:06 +02:00
// Only update if the name is still the default pattern
if ( fs . name . match ( /^Food Source \d+$/ ) ) {
fs . name = ` Food Source ${ index + 1 } ` ;
const titleElement = document . getElementById ( ` food-title- ${ fs . id } ` ) ;
if ( titleElement ) {
titleElement . value = fs . name ;
}
2025-06-26 11:09:29 +02:00
}
} ) ;
}
updateAddButton ( ) {
const addBtn = document . getElementById ( 'addFoodBtn' ) ;
if ( addBtn ) {
const remaining = this . maxFoodSources - this . foodSources . length ;
2025-06-26 13:02:28 +02:00
const buttonText = addBtn . querySelector ( 'span:last-child' ) ;
2025-06-26 11:09:29 +02:00
2025-06-26 13:02:28 +02:00
if ( remaining <= 0 ) {
// Disable button and show max reached message
addBtn . disabled = true ;
if ( buttonText ) {
buttonText . textContent = ` Maximum ${ this . maxFoodSources } sources reached ` ;
}
} else {
// Enable button with normal text
addBtn . disabled = false ;
if ( buttonText ) {
buttonText . textContent = 'Add another food source' ;
2025-06-26 11:09:29 +02:00
}
}
}
}
2025-06-26 14:32:32 +02:00
updateRemoveButtons ( ) {
// Show/hide remove buttons based on whether we have more than one source
const hasMultipleSources = this . foodSources . length > 1 ;
this . foodSources . forEach ( fs => {
const removeBtn = document . getElementById ( ` remove- ${ fs . id } ` ) ;
if ( removeBtn ) {
removeBtn . style . display = hasMultipleSources ? 'block' : 'none' ;
}
} ) ;
}
2025-06-26 11:09:29 +02:00
renderFoodSource ( foodSource ) {
const container = document . getElementById ( 'foodSources' ) ;
if ( ! container ) return ;
2026-02-06 12:52:13 +01:00
const isChart = foodSource . chartType === 'kibble' || foodSource . chartType === 'mer' ;
2025-11-12 18:34:26 +01:00
const energyReadonlyAttr = isChart ? 'readonly' : '' ;
const energyTitle = isChart ? 'Chart-based food: kcal locked' : 'Enter energy content' ;
const unitDisabledAttr = isChart ? 'disabled' : '' ;
2025-06-26 11:09:29 +02:00
const cardHTML = `
<div class="dog-calculator-food-source-card" id="foodSource- ${ foodSource . id } ">
<div class="dog-calculator-food-source-header">
2025-06-26 12:34:06 +02:00
<input type="text" class="dog-calculator-food-source-name-input" id="food-title- ${ foodSource . id } " value=" ${ foodSource . name } " placeholder="Enter food name" maxlength="50" title="Click to edit food source name">
2025-06-26 14:32:32 +02:00
<button class="dog-calculator-remove-food-btn" id="remove- ${ foodSource . id } " type="button" title="Remove this food source" style="display: ${ this . foodSources . length > 1 ? 'block' : 'none' } ">× </button>
2025-06-26 11:09:29 +02:00
</div>
<div class="dog-calculator-input-group">
<div class="dog-calculator-form-group">
2025-06-26 13:02:28 +02:00
<label for="energy- ${ foodSource . id } ">Energy Content:</label>
2025-11-12 18:34:26 +01:00
<input type="number" id="energy- ${ foodSource . id } " ${ energyReadonlyAttr } title=" ${ energyTitle } " min="1" step="1" placeholder="Enter energy content" value=" ${ foodSource . energy } ">
2025-06-26 11:09:29 +02:00
</div>
<div class="dog-calculator-form-group">
<label for="energy-unit- ${ foodSource . id } ">Unit:</label>
2025-11-12 18:34:26 +01:00
<select id="energy-unit- ${ foodSource . id } " class="dog-calculator-unit-select" ${ unitDisabledAttr } title=" ${ isChart ? 'Chart-based food: unit locked' : 'Select energy unit' } ">
2025-06-26 11:09:29 +02:00
<option value="kcal100g" ${ foodSource . energyUnit === 'kcal100g' ? 'selected' : '' } >kcal/100g</option>
<option value="kcalkg" ${ foodSource . energyUnit === 'kcalkg' ? 'selected' : '' } >kcal/kg</option>
<option value="kcalcup" ${ foodSource . energyUnit === 'kcalcup' ? 'selected' : '' } >kcal/cup</option>
<option value="kcalcan" ${ foodSource . energyUnit === 'kcalcan' ? 'selected' : '' } >kcal/can</option>
</select>
</div>
</div>
2025-06-26 14:32:32 +02:00
<div id="energy-error- ${ foodSource . id } " class="dog-calculator-error dog-calculator-hidden">Please enter a valid energy content</div>
2025-06-26 11:09:29 +02:00
<div class="dog-calculator-percentage-group">
<label class="dog-calculator-percentage-label" for="percentage-slider- ${ foodSource . id } ">
Percentage of Diet: <span id="percentage-display- ${ foodSource . id } "> ${ foodSource . percentage } %</span>
2025-06-26 11:19:29 +02:00
<span class="dog-calculator-lock-icon unlocked" id="lock- ${ foodSource . id } " title="Lock this percentage">🔒</span>
2025-06-26 11:09:29 +02:00
</label>
<div class="dog-calculator-percentage-input-group">
<input type="range" id="percentage-slider- ${ foodSource . id } " class="dog-calculator-percentage-slider"
min="0" max="100" value=" ${ foodSource . percentage } ">
<input type="number" id="percentage-input- ${ foodSource . id } " class="dog-calculator-percentage-input"
min="0" max="100" value=" ${ foodSource . percentage } ">
</div>
</div>
</div>
` ;
container . insertAdjacentHTML ( 'beforeend' , cardHTML ) ;
// Bind events for the new food source
this . bindFoodSourceEvents ( foodSource . id ) ;
}
bindFoodSourceEvents ( id ) {
2025-06-26 12:34:06 +02:00
// Name input events
const nameInput = document . getElementById ( ` food-title- ${ id } ` ) ;
2025-06-26 11:09:29 +02:00
// Energy input events
const energyInput = document . getElementById ( ` energy- ${ id } ` ) ;
const energyUnitSelect = document . getElementById ( ` energy-unit- ${ id } ` ) ;
const percentageSlider = document . getElementById ( ` percentage-slider- ${ id } ` ) ;
const percentageInput = document . getElementById ( ` percentage-input- ${ id } ` ) ;
const removeBtn = document . getElementById ( ` remove- ${ id } ` ) ;
2025-06-26 11:19:29 +02:00
const lockBtn = document . getElementById ( ` lock- ${ id } ` ) ;
2025-06-26 11:09:29 +02:00
2025-06-26 12:34:06 +02:00
if ( nameInput ) {
nameInput . addEventListener ( 'input' , ( ) => {
const newName = nameInput . value . trim ( ) || ` Food Source ${ this . foodSources . findIndex ( fs => fs . id === id ) + 1 } ` ;
this . updateFoodSourceData ( id , 'name' , newName ) ;
this . updateFoodCalculations ( ) ; // This will refresh the food amount breakdown with new names
2025-11-12 18:39:10 +01:00
this . saveStateToStorage ( ) ;
2025-06-26 12:34:06 +02:00
} ) ;
nameInput . addEventListener ( 'blur' , ( ) => {
// If field is empty, restore default name
if ( ! nameInput . value . trim ( ) ) {
const defaultName = ` Food Source ${ this . foodSources . findIndex ( fs => fs . id === id ) + 1 } ` ;
nameInput . value = defaultName ;
this . updateFoodSourceData ( id , 'name' , defaultName ) ;
this . updateFoodCalculations ( ) ;
2025-11-12 18:39:10 +01:00
this . saveStateToStorage ( ) ;
2025-06-26 12:34:06 +02:00
}
} ) ;
}
2025-11-12 18:34:26 +01:00
if ( energyInput && ! energyInput . hasAttribute ( 'readonly' ) ) {
2025-06-26 11:09:29 +02:00
energyInput . addEventListener ( 'input' , ( ) => {
this . updateFoodSourceData ( id , 'energy' , energyInput . value ) ;
2025-11-12 16:44:43 +01:00
// If kibble reference changed, recompute daily target
if ( id === this . kibbleRefId ) {
this . updateCalorieCalculations ( ) ;
}
2025-08-18 14:36:25 +02:00
// Auto-select cups when entering energy for kcal/cup
const foodSource = this . foodSources . find ( fs => fs . id === id ) ;
if ( foodSource && foodSource . energyUnit === 'kcalcup' && parseFloat ( energyInput . value ) > 0 ) {
2025-11-12 17:00:45 +01:00
// Cups display removed; default to grams
2025-08-18 14:36:25 +02:00
const unitSelect = document . getElementById ( 'unit' ) ;
2025-11-12 17:00:45 +01:00
if ( unitSelect ) {
unitSelect . value = 'g' ;
unitSelect . setAttribute ( 'value' , 'g' ) ;
this . setActiveUnitButton ( 'g' ) ;
2025-08-18 14:36:25 +02:00
}
}
2025-11-12 17:00:45 +01:00
this . updateFoodCalculations ( ) ;
2025-11-12 18:39:10 +01:00
this . saveStateToStorage ( ) ;
2025-06-26 11:09:29 +02:00
} ) ;
energyInput . addEventListener ( 'blur' , ( ) => this . validateFoodSourceEnergy ( id ) ) ;
}
2025-11-12 18:34:26 +01:00
if ( energyUnitSelect && ! energyUnitSelect . hasAttribute ( 'disabled' ) ) {
2025-06-26 11:09:29 +02:00
energyUnitSelect . addEventListener ( 'change' , ( ) => {
this . updateFoodSourceData ( id , 'energyUnit' , energyUnitSelect . value ) ;
2025-11-12 16:44:43 +01:00
if ( id === this . kibbleRefId ) {
this . updateCalorieCalculations ( ) ;
}
2025-08-18 14:36:25 +02:00
// Auto-select the most appropriate unit based on energy unit
const unitSelect = document . getElementById ( 'unit' ) ;
const energyInput = document . getElementById ( ` energy- ${ id } ` ) ;
if ( unitSelect ) {
switch ( energyUnitSelect . value ) {
case 'kcalcup' :
2025-11-12 17:00:45 +01:00
// Cups display not available; default to grams
unitSelect . value = 'g' ;
this . setActiveUnitButton ( 'g' ) ;
2025-08-18 14:36:25 +02:00
this . updateFoodCalculations ( ) ;
2025-11-12 18:39:10 +01:00
this . saveStateToStorage ( ) ;
break ;
case 'kcal100g' :
// For kcal/100g, select grams
unitSelect . value = 'g' ;
this . setActiveUnitButton ( 'g' ) ;
this . updateFoodCalculations ( ) ;
this . saveStateToStorage ( ) ;
break ;
case 'kcalkg' :
// For kcal/kg, also select grams (or could be kg)
unitSelect . value = 'g' ;
this . setActiveUnitButton ( 'g' ) ;
this . updateFoodCalculations ( ) ;
this . saveStateToStorage ( ) ;
break ;
case 'kcalcan' :
// For kcal/can, use grams as default
unitSelect . value = 'g' ;
this . setActiveUnitButton ( 'g' ) ;
this . updateFoodCalculations ( ) ;
this . saveStateToStorage ( ) ;
break ;
2025-08-18 14:36:25 +02:00
}
2025-11-12 18:39:10 +01:00
} else {
// No unit select, just update calculations
this . updateFoodCalculations ( ) ;
this . saveStateToStorage ( ) ;
}
} ) ;
2025-06-26 11:09:29 +02:00
}
if ( percentageSlider ) {
percentageSlider . addEventListener ( 'input' , ( ) => {
2025-06-26 12:21:18 +02:00
const requestedValue = parseInt ( percentageSlider . value ) ;
const result = this . validatePercentageChange ( id , requestedValue ) ;
2025-06-26 11:47:33 +02:00
2025-06-26 12:21:18 +02:00
if ( result . isValid ) {
this . applyValidatedChanges ( result ) ;
2025-06-26 11:47:33 +02:00
}
2025-06-26 12:21:18 +02:00
// Always refresh to ensure valid state
this . refreshAllPercentageUI ( ) ;
2025-06-26 11:09:29 +02:00
} ) ;
}
if ( percentageInput ) {
percentageInput . addEventListener ( 'change' , ( ) => {
2025-06-26 12:21:18 +02:00
const requestedValue = parseInt ( percentageInput . value ) || 0 ;
const result = this . validatePercentageChange ( id , requestedValue ) ;
2025-06-26 11:47:33 +02:00
2025-06-26 12:21:18 +02:00
if ( result . isValid ) {
this . applyValidatedChanges ( result ) ;
}
this . refreshAllPercentageUI ( ) ;
2025-06-26 11:09:29 +02:00
} ) ;
}
if ( removeBtn ) {
removeBtn . addEventListener ( 'click' , ( ) => this . removeFoodSource ( id ) ) ;
}
2025-06-26 11:19:29 +02:00
if ( lockBtn ) {
lockBtn . addEventListener ( 'click' , ( ) => this . toggleLock ( id ) ) ;
}
}
toggleLock ( id ) {
const foodSource = this . foodSources . find ( fs => fs . id === id ) ;
if ( ! foodSource ) return ;
// Check if we're trying to lock the last unlocked source
const unlockedSources = this . foodSources . filter ( fs => ! fs . isLocked ) ;
if ( unlockedSources . length === 1 && unlockedSources [ 0 ] . id === id ) {
// Cannot lock the last unlocked source
alert ( 'At least one food source must remain flexible for percentage adjustments.' ) ;
return ;
}
// Toggle lock state
foodSource . isLocked = ! foodSource . isLocked ;
this . updateLockIcon ( id ) ;
this . updateLockStates ( ) ;
2025-06-26 12:21:18 +02:00
this . refreshAllPercentageUI ( ) ;
2025-11-12 18:39:10 +01:00
this . saveStateToStorage ( ) ;
2025-06-26 11:19:29 +02:00
}
updateLockIcon ( id ) {
const foodSource = this . foodSources . find ( fs => fs . id === id ) ;
const lockIcon = document . getElementById ( ` lock- ${ id } ` ) ;
if ( ! lockIcon || ! foodSource ) return ;
if ( foodSource . isLocked ) {
lockIcon . classList . remove ( 'unlocked' ) ;
lockIcon . classList . add ( 'locked' ) ;
lockIcon . title = 'Unlock this percentage' ;
} else {
lockIcon . classList . remove ( 'locked' ) ;
lockIcon . classList . add ( 'unlocked' ) ;
lockIcon . title = 'Lock this percentage' ;
}
}
updateLockStates ( ) {
const unlockedSources = this . foodSources . filter ( fs => ! fs . isLocked ) ;
// Update lock icon states - disable lock for last unlocked source
this . foodSources . forEach ( fs => {
const lockIcon = document . getElementById ( ` lock- ${ fs . id } ` ) ;
if ( lockIcon ) {
if ( ! fs . isLocked && unlockedSources . length === 1 ) {
lockIcon . classList . add ( 'disabled' ) ;
lockIcon . title = 'Cannot lock - at least one source must remain flexible' ;
} else {
lockIcon . classList . remove ( 'disabled' ) ;
lockIcon . title = fs . isLocked ? 'Unlock this percentage' : 'Lock this percentage' ;
}
}
} ) ;
2025-06-26 11:27:03 +02:00
// Update percentage constraints based on lock states
2025-06-26 12:21:18 +02:00
this . refreshAllPercentageUI ( ) ;
2025-06-26 11:09:29 +02:00
}
updateFoodSourceData ( id , field , value ) {
const foodSource = this . foodSources . find ( fs => fs . id === id ) ;
if ( foodSource ) {
foodSource [ field ] = value ;
}
}
validateFoodSourceEnergy ( id ) {
const energyInput = document . getElementById ( ` energy- ${ id } ` ) ;
const energyUnitSelect = document . getElementById ( ` energy-unit- ${ id } ` ) ;
const errorElement = document . getElementById ( ` energy-error- ${ id } ` ) ;
if ( ! energyInput || ! energyUnitSelect || ! errorElement ) return ;
const energy = parseFloat ( energyInput . value ) ;
const unit = energyUnitSelect . value ;
let minValue = 1 ;
switch ( unit ) {
case 'kcal100g' : minValue = 1 ; break ;
case 'kcalkg' : minValue = 10 ; break ;
case 'kcalcup' : minValue = 50 ; break ;
case 'kcalcan' : minValue = 100 ; break ;
}
if ( ! this . validateInput ( energy , minValue ) ) {
errorElement . classList . remove ( 'dog-calculator-hidden' ) ;
} else {
errorElement . classList . add ( 'dog-calculator-hidden' ) ;
}
}
2025-06-08 22:27:21 +02:00
bindEvents ( ) {
const weightInput = document . getElementById ( 'weight' ) ;
const dogTypeSelect = document . getElementById ( 'dogType' ) ;
2025-11-12 16:44:43 +01:00
const ageInput = document . getElementById ( 'ageMonths' ) ;
2025-06-08 22:27:21 +02:00
const daysInput = document . getElementById ( 'days' ) ;
const unitSelect = document . getElementById ( 'unit' ) ;
const unitToggle = document . getElementById ( 'unitToggle' ) ;
2025-06-26 11:09:29 +02:00
const addFoodBtn = document . getElementById ( 'addFoodBtn' ) ;
2025-06-08 22:27:21 +02:00
if ( weightInput ) {
2026-01-28 15:51:28 +01:00
weightInput . addEventListener ( 'input' , ( ) => { this . updateCalorieCalculations ( ) ; this . saveStateToStorage ( ) ; } ) ;
weightInput . addEventListener ( 'blur' , ( ) => { this . validateWeight ( ) ; this . saveStateToStorage ( ) ; } ) ;
2025-06-08 22:27:21 +02:00
}
if ( dogTypeSelect ) dogTypeSelect . addEventListener ( 'change' , ( ) => this . updateCalorieCalculations ( ) ) ;
2025-11-12 16:44:43 +01:00
// Kayafied: age input drives energy target
if ( ageInput ) {
2025-11-12 18:39:10 +01:00
ageInput . addEventListener ( 'input' , ( ) => { this . updateCalorieCalculations ( ) ; this . saveStateToStorage ( ) ; } ) ;
ageInput . addEventListener ( 'blur' , ( ) => { this . updateCalorieCalculations ( ) ; this . saveStateToStorage ( ) ; } ) ;
2025-11-12 16:44:43 +01:00
}
2025-06-08 22:27:21 +02:00
if ( daysInput ) {
2025-06-26 15:55:39 +02:00
daysInput . addEventListener ( 'input' , ( ) => {
this . updateDayLabel ( ) ;
this . updateFoodCalculations ( ) ;
2025-11-12 18:39:10 +01:00
this . saveStateToStorage ( ) ;
2025-06-26 15:55:39 +02:00
} ) ;
2025-06-08 22:27:21 +02:00
daysInput . addEventListener ( 'blur' , ( ) => this . validateDays ( ) ) ;
}
2025-11-12 18:39:10 +01:00
if ( unitSelect ) unitSelect . addEventListener ( 'change' , ( ) => { this . updateFoodCalculations ( ) ; this . saveStateToStorage ( ) ; } ) ;
2025-06-26 16:34:38 +02:00
// Unit button event listeners
const unitButtons = document . querySelectorAll ( '.dog-calculator-unit-btn' ) ;
unitButtons . forEach ( button => {
button . addEventListener ( 'click' , ( e ) => {
const selectedUnit = e . target . dataset . unit ;
this . setActiveUnitButton ( selectedUnit ) ;
// Update hidden select to trigger existing logic
if ( unitSelect ) {
unitSelect . value = selectedUnit ;
this . updateFoodCalculations ( ) ;
2025-11-12 18:39:10 +01:00
this . saveStateToStorage ( ) ;
2025-06-26 16:34:38 +02:00
}
} ) ;
} ) ;
2025-06-08 22:27:21 +02:00
if ( unitToggle ) unitToggle . addEventListener ( 'change' , ( ) => this . toggleUnits ( ) ) ;
2025-06-08 23:45:32 +02:00
2025-06-26 11:09:29 +02:00
if ( addFoodBtn ) addFoodBtn . addEventListener ( 'click' , ( ) => this . addFoodSource ( ) ) ;
2025-08-18 12:45:44 +02:00
// Feeding configuration event listeners
const showDaily = document . getElementById ( 'showDaily' ) ;
const showPerMeal = document . getElementById ( 'showPerMeal' ) ;
const mealsPerDayInput = document . getElementById ( 'mealsPerDay' ) ;
const mealInputGroup = document . getElementById ( 'mealInputGroup' ) ;
if ( showDaily ) {
showDaily . addEventListener ( 'change' , ( ) => {
if ( showDaily . checked ) {
this . showPerMeal = false ;
if ( mealInputGroup ) mealInputGroup . style . display = 'none' ;
this . updateDayLabel ( ) ;
this . updateFoodCalculations ( ) ;
}
} ) ;
}
if ( showPerMeal ) {
showPerMeal . addEventListener ( 'change' , ( ) => {
if ( showPerMeal . checked ) {
this . showPerMeal = true ;
if ( mealInputGroup ) mealInputGroup . style . display = 'inline-flex' ;
this . updateDayLabel ( ) ;
this . updateFoodCalculations ( ) ;
}
} ) ;
}
if ( mealsPerDayInput ) {
mealsPerDayInput . addEventListener ( 'input' , ( ) => {
const meals = parseInt ( mealsPerDayInput . value ) ;
if ( meals && meals >= 1 && meals <= 10 ) {
this . mealsPerDay = meals ;
if ( this . showPerMeal ) {
this . updateDayLabel ( ) ;
this . updateFoodCalculations ( ) ;
}
}
} ) ;
mealsPerDayInput . addEventListener ( 'blur' , ( ) => {
if ( ! mealsPerDayInput . value || parseInt ( mealsPerDayInput . value ) < 1 ) {
mealsPerDayInput . value = 2 ;
this . mealsPerDay = 2 ;
if ( this . showPerMeal ) {
this . updateDayLabel ( ) ;
this . updateFoodCalculations ( ) ;
}
}
} ) ;
}
2025-06-08 23:45:32 +02:00
// Modal event listeners
const shareBtn = document . getElementById ( 'shareBtn' ) ;
const shareModalClose = document . getElementById ( 'shareModalClose' ) ;
if ( shareBtn ) shareBtn . addEventListener ( 'click' , ( ) => this . showShareModal ( ) ) ;
if ( shareModalClose ) shareModalClose . addEventListener ( 'click' , ( ) => this . hideShareModal ( ) ) ;
// Share buttons
const shareFacebook = document . getElementById ( 'shareFacebook' ) ;
const shareTwitter = document . getElementById ( 'shareTwitter' ) ;
const shareLinkedIn = document . getElementById ( 'shareLinkedIn' ) ;
const shareEmail = document . getElementById ( 'shareEmail' ) ;
const shareCopy = document . getElementById ( 'shareCopy' ) ;
if ( shareFacebook ) shareFacebook . addEventListener ( 'click' , ( ) => this . shareToFacebook ( ) ) ;
if ( shareTwitter ) shareTwitter . addEventListener ( 'click' , ( ) => this . shareToTwitter ( ) ) ;
if ( shareLinkedIn ) shareLinkedIn . addEventListener ( 'click' , ( ) => this . shareToLinkedIn ( ) ) ;
if ( shareEmail ) shareEmail . addEventListener ( 'click' , ( ) => this . shareViaEmail ( ) ) ;
if ( shareCopy ) shareCopy . addEventListener ( 'click' , ( ) => this . copyShareLink ( ) ) ;
2025-10-28 09:58:20 +01:00
// Embed copy buttons removed (embedding disabled)
2025-06-08 23:45:32 +02:00
// Close modals on outside click
const shareModal = document . getElementById ( 'shareModal' ) ;
if ( shareModal ) {
shareModal . addEventListener ( 'click' , ( e ) => {
if ( e . target === shareModal ) this . hideShareModal ( ) ;
} ) ;
}
2025-10-28 09:58:20 +01:00
// Embed modal removed
2025-06-08 22:27:21 +02:00
}
toggleUnits ( ) {
const toggle = document . getElementById ( 'unitToggle' ) ;
this . isImperial = toggle . checked ;
this . updateUnitLabels ( ) ;
this . convertExistingValues ( ) ;
this . updateCalorieCalculations ( ) ;
}
updateUnitLabels ( ) {
const metricLabel = document . getElementById ( 'metricLabel' ) ;
const imperialLabel = document . getElementById ( 'imperialLabel' ) ;
const weightLabel = document . getElementById ( 'weightLabel' ) ;
const weightInput = document . getElementById ( 'weight' ) ;
const unitSelect = document . getElementById ( 'unit' ) ;
if ( metricLabel && imperialLabel ) {
metricLabel . classList . toggle ( 'active' , ! this . isImperial ) ;
imperialLabel . classList . toggle ( 'active' , this . isImperial ) ;
}
2025-11-12 17:00:45 +01:00
// Kaya: restrict to metric g/kg only
if ( unitSelect ) {
unitSelect . innerHTML = '<option value="g">grams (g)</option>' +
'<option value="kg">kilograms (kg)</option>' ;
if ( ! unitSelect . value || ( unitSelect . value !== 'g' && unitSelect . value !== 'kg' ) ) {
unitSelect . value = 'g' ;
this . setActiveUnitButton ( 'g' ) ;
2025-06-08 22:27:21 +02:00
}
}
}
convertExistingValues ( ) {
const weightInput = document . getElementById ( 'weight' ) ;
if ( weightInput && weightInput . value ) {
const currentWeight = parseFloat ( weightInput . value ) ;
if ( ! isNaN ( currentWeight ) ) {
if ( this . isImperial ) {
weightInput . value = this . formatNumber ( currentWeight * 2.20462 , 1 ) ;
} else {
weightInput . value = this . formatNumber ( currentWeight / 2.20462 , 1 ) ;
}
}
}
}
getWeightInKg ( ) {
const weightInput = document . getElementById ( 'weight' ) ;
if ( ! weightInput || ! weightInput . value ) return null ;
const weight = parseFloat ( weightInput . value ) ;
if ( isNaN ( weight ) ) return null ;
return this . isImperial ? weight / 2.20462 : weight ;
}
calculateRER ( weightKg ) {
return 70 * Math . pow ( weightKg , 0.75 ) ;
}
calculateMER ( rer , factor ) {
return rer * factor ;
}
2025-08-18 15:33:44 +02:00
// Get the range multipliers for each life stage
getLifeStageRange ( factor ) {
// Define ranges based on the reference image
const ranges = {
'3.0' : { min : 3.0 , max : 3.0 } , // Puppy 0-4 months (no range)
'2.0' : { min : 2.0 , max : 2.0 } , // Puppy 4m-adult OR Working light (no range for puppies)
'1.2' : { min : 1.2 , max : 1.4 } , // Adult inactive/obese
'1.6' : { min : 1.4 , max : 1.6 } , // Adult neutered/spayed
'1.8' : { min : 1.6 , max : 1.8 } , // Adult intact
'1.0' : { min : 1.0 , max : 1.0 } , // Weight loss (fixed)
'1.7' : { min : 1.2 , max : 1.8 } , // Weight gain (wide range)
'5.0' : { min : 5.0 , max : 5.0 } , // Working heavy (upper bound)
'1.1' : { min : 1.1 , max : 1.1 } // Senior (no range)
} ;
const key = factor . toFixed ( 1 ) ;
return ranges [ key ] || { min : factor , max : factor } ;
}
2025-06-08 22:27:21 +02:00
validateInput ( value , min = 0 , isInteger = false ) {
const num = parseFloat ( value ) ;
if ( isNaN ( num ) || num < min ) return false ;
if ( isInteger && ! Number . isInteger ( num ) ) return false ;
return true ;
}
showError ( elementId , show = true ) {
const errorElement = document . getElementById ( elementId ) ;
if ( errorElement ) {
if ( show ) {
errorElement . classList . remove ( 'dog-calculator-hidden' ) ;
} else {
errorElement . classList . add ( 'dog-calculator-hidden' ) ;
}
}
}
2025-08-18 14:36:25 +02:00
convertUnits ( grams , unit , foodSource = null ) {
2025-06-08 22:27:21 +02:00
switch ( unit ) {
case 'kg' :
return grams / 1000 ;
case 'oz' :
return grams / 28.3495 ;
case 'lb' :
return grams / 453.592 ;
2025-08-18 14:36:25 +02:00
case 'cups' :
// For cups, we need to convert from grams worth of calories to cups
if ( foodSource && foodSource . energyUnit === 'kcalcup' && foodSource . energy ) {
// Get calories per 100g for this food
const caloriesPerGram = this . getFoodSourceEnergyPer100g ( foodSource ) / 100 ;
// Calculate total calories represented by these grams
const totalCalories = grams * caloriesPerGram ;
// Divide by calories per cup to get number of cups
const caloriesPerCup = parseFloat ( foodSource . energy ) ;
return totalCalories / caloriesPerCup ;
}
return null ; // Cannot convert to cups without kcal/cup
2025-06-08 22:27:21 +02:00
default :
return grams ;
}
}
formatNumber ( num , decimals = 0 ) {
if ( decimals === 0 ) {
return Math . round ( num ) . toString ( ) ;
}
return num . toFixed ( decimals ) . replace ( /\.?0+$/ , '' ) ;
}
validateWeight ( ) {
const weightKg = this . getWeightInKg ( ) ;
if ( weightKg !== null && weightKg < 0.1 ) {
this . showError ( 'weightError' , true ) ;
} else {
this . showError ( 'weightError' , false ) ;
}
}
validateDays ( ) {
const days = document . getElementById ( 'days' ) ? . value ;
if ( days && ! this . validateInput ( days , 1 , true ) ) {
this . showError ( 'daysError' , true ) ;
} else {
this . showError ( 'daysError' , false ) ;
}
}
2025-06-26 15:55:39 +02:00
updateDayLabel ( ) {
const days = document . getElementById ( 'days' ) ? . value ;
const dayLabel = document . getElementById ( 'dayLabel' ) ;
2025-08-18 12:45:44 +02:00
const mealNote = document . getElementById ( 'mealNote' ) ;
2025-06-26 15:55:39 +02:00
if ( dayLabel && days ) {
const numDays = parseInt ( days ) ;
dayLabel . textContent = numDays === 1 ? 'day' : 'days' ;
}
2025-08-18 12:45:44 +02:00
if ( mealNote ) {
if ( this . showPerMeal && days ) {
const numDays = parseInt ( days ) ;
const totalMeals = numDays * this . mealsPerDay ;
mealNote . textContent = ` ( ${ totalMeals } meal ${ totalMeals === 1 ? '' : 's' } total) ` ;
mealNote . style . display = 'inline' ;
} else {
mealNote . style . display = 'none' ;
}
}
2025-06-26 15:55:39 +02:00
}
2025-06-26 16:34:38 +02:00
setActiveUnitButton ( unit ) {
const unitButtons = document . querySelectorAll ( '.dog-calculator-unit-btn' ) ;
unitButtons . forEach ( button => {
button . classList . remove ( 'active' ) ;
if ( button . dataset . unit === unit ) {
button . classList . add ( 'active' ) ;
}
} ) ;
}
2025-06-08 22:27:21 +02:00
updateCalorieCalculations ( ) {
2025-11-12 18:00:36 +01:00
// Kaya-specific: only track age and trigger recompute
2025-11-12 16:44:43 +01:00
const ageInput = document . getElementById ( 'ageMonths' ) ;
const ageClampNote = document . getElementById ( 'ageClampNote' ) ;
2025-06-08 22:27:21 +02:00
2025-11-12 16:44:43 +01:00
if ( ageClampNote ) ageClampNote . classList . add ( 'dog-calculator-hidden' ) ;
2025-11-12 18:00:36 +01:00
if ( ! ageInput || ageInput . value === '' ) {
this . currentAge = null ;
this . updateFoodCalculations ( ) ;
2025-06-08 22:27:21 +02:00
return ;
}
2025-11-12 16:44:43 +01:00
let age = parseFloat ( ageInput . value ) ;
if ( isNaN ( age ) ) {
2025-11-12 18:00:36 +01:00
this . currentAge = null ;
this . updateFoodCalculations ( ) ;
2025-06-08 22:27:21 +02:00
return ;
}
2025-11-12 16:44:43 +01:00
if ( age < 2 ) { age = 2 ; if ( ageClampNote ) ageClampNote . classList . remove ( 'dog-calculator-hidden' ) ; }
if ( age > 12 ) { age = 12 ; if ( ageClampNote ) ageClampNote . classList . remove ( 'dog-calculator-hidden' ) ; }
2025-11-12 18:00:36 +01:00
this . currentAge = age ;
2025-06-08 22:27:21 +02:00
this . updateFoodCalculations ( ) ;
}
2025-11-12 16:44:43 +01:00
// Kaya: monthly table with interpolation between months
getKayaKibbleGramsForAge ( ageMonths ) {
// Precomputed monthly values for 30 kg (g/day)
const table = {
2 : 250 ,
3 : 330 ,
4 : 365 ,
5 : 382 ,
6 : 400 ,
7 : 405 ,
8 : 410 ,
9 : 410 ,
10 : 410 ,
11 : 408 ,
12 : 405
} ;
// Exact integer month
const lower = Math . floor ( ageMonths ) ;
const upper = Math . ceil ( ageMonths ) ;
if ( lower === upper ) return table [ lower ] || 0 ;
// Linear interpolation between bounds
const lowerVal = table [ lower ] || 0 ;
const upperVal = table [ upper ] || 0 ;
const t = ( ageMonths - lower ) / ( upper - lower ) ;
return lowerVal + ( upperVal - lowerVal ) * t ;
}
2026-01-28 15:51:28 +01:00
// Kaya: MER-based grams/day for Fred & Felia (kibble remains chart-based)
getKayaMerFactorForAge ( ageMonths ) {
if ( ageMonths === null || ageMonths === undefined ) return null ;
if ( ageMonths < 4 ) return CALCULATOR _CONFIG . kayaMerFactorUnder4Months ;
return CALCULATOR _CONFIG . kayaMerFactorFrom4Months ;
}
getKayaCurrentWeightKg ( ) {
const weightInput = document . getElementById ( 'weight' ) ;
if ( ! weightInput ) return CALCULATOR _CONFIG . kayaMerDefaultWeightKg ;
const weightKg = this . getWeightInKg ( ) ;
if ( ! weightKg || weightKg < 0.1 ) return null ;
return weightKg ;
}
getKayaMerCaloriesForAge ( ageMonths ) {
const factor = this . getKayaMerFactorForAge ( ageMonths ) ;
if ( ! factor ) return null ;
const weightKg = this . getKayaCurrentWeightKg ( ) ;
if ( ! weightKg ) return null ;
const rer = this . calculateRER ( weightKg ) ;
return this . calculateMER ( rer , factor ) ;
}
getKayaMerBasedGramsForFood ( ageMonths , energyPer100g ) {
if ( ! energyPer100g || energyPer100g <= 0.1 ) return null ;
const merCalories = this . getKayaMerCaloriesForAge ( ageMonths ) ;
if ( ! merCalories ) return null ;
const kcalPerGram = energyPer100g / 100 ;
return merCalories / kcalPerGram ;
2025-11-12 18:00:36 +01:00
}
2025-11-12 17:00:45 +01:00
2025-08-18 14:36:25 +02:00
updateCupsButtonState ( ) {
2025-11-12 18:00:36 +01:00
// Cups UI is not used in this configuration
return ;
2025-08-18 14:36:25 +02:00
}
2025-06-08 22:27:21 +02:00
updateFoodCalculations ( ) {
2025-11-12 18:00:36 +01:00
// Chart-first: no MER check
const hasRange = false ;
2025-08-18 15:33:44 +02:00
2025-06-08 22:27:21 +02:00
const daysInput = document . getElementById ( 'days' ) ;
const unitSelect = document . getElementById ( 'unit' ) ;
const dailyFoodResults = document . getElementById ( 'dailyFoodResults' ) ;
const dailyFoodValue = document . getElementById ( 'dailyFoodValue' ) ;
2025-06-26 12:29:35 +02:00
const foodAmountsSection = document . getElementById ( 'foodAmountsSection' ) ;
const foodAmountsList = document . getElementById ( 'foodAmountsList' ) ;
const totalAmountDisplay = document . getElementById ( 'totalAmountDisplay' ) ;
2025-06-26 11:09:29 +02:00
const foodBreakdownResults = document . getElementById ( 'foodBreakdownResults' ) ;
const foodBreakdownList = document . getElementById ( 'foodBreakdownList' ) ;
2025-08-18 12:45:44 +02:00
const feedingConfig = document . getElementById ( 'feedingConfig' ) ;
2025-08-18 14:36:25 +02:00
// Update cups button state
this . updateCupsButtonState ( ) ;
2025-06-08 22:27:21 +02:00
2025-06-26 12:29:35 +02:00
if ( ! daysInput || ! unitSelect || ! dailyFoodResults || ! dailyFoodValue || ! foodAmountsSection ) {
2025-06-08 22:27:21 +02:00
return ;
}
const days = daysInput . value ;
2025-08-18 14:36:25 +02:00
let unit = unitSelect . value ;
// Failsafe: if unit is empty string but cups button is active, use 'cups'
if ( ! unit || unit === '' ) {
const activeButton = document . querySelector ( '.dog-calculator-unit-btn.active' ) ;
if ( activeButton ) {
unit = activeButton . dataset . unit || 'g' ;
} else {
unit = 'g' ; // Default fallback
}
}
const unitLabel = unit === 'g' ? 'g' : unit === 'kg' ? 'kg' : unit === 'oz' ? 'oz' : unit === 'lb' ? 'lb' : 'cups' ;
const decimals = unit === 'g' ? 0 : unit === 'kg' ? 2 : unit === 'cups' ? 1 : 1 ;
// Debug: log what unit is being used
console . log ( 'UpdateFoodCalculations - unit:' , unit , 'unitLabel:' , unitLabel ) ;
2025-06-08 22:27:21 +02:00
2025-11-12 18:34:26 +01:00
// Determine frequency suffix for display (will adjust per-item below)
2025-08-18 12:45:44 +02:00
const frequencySuffix = this . showPerMeal ? '/meal' : '/day' ;
2025-06-26 11:09:29 +02:00
// Clear all food source errors first
this . foodSources . forEach ( fs => {
this . showError ( ` energy-error- ${ fs . id } ` , false ) ;
} ) ;
2025-06-08 22:27:21 +02:00
this . showError ( 'daysError' , false ) ;
2026-01-28 15:51:28 +01:00
this . showError ( 'weightError' , false ) ;
2025-06-08 22:27:21 +02:00
2025-06-26 11:09:29 +02:00
// Validate days input
2025-06-08 22:27:21 +02:00
if ( ! days || ! this . validateInput ( days , 1 , true ) ) {
if ( days ) this . showError ( 'daysError' , true ) ;
2025-06-26 12:29:35 +02:00
foodAmountsSection . style . display = 'none' ;
2025-06-26 11:09:29 +02:00
dailyFoodResults . style . display = 'none' ;
if ( foodBreakdownResults ) foodBreakdownResults . style . display = 'none' ;
2025-08-18 12:45:44 +02:00
if ( feedingConfig ) feedingConfig . style . display = 'none' ;
2025-06-26 16:34:38 +02:00
// Hide unit buttons when validation fails
const unitButtons = document . getElementById ( 'unitButtons' ) ;
if ( unitButtons ) unitButtons . style . display = 'none' ;
2025-06-08 22:27:21 +02:00
return ;
}
const numDays = parseInt ( days ) ;
2025-11-12 18:00:36 +01:00
// Require a valid age for chart-first outputs
const ageInput = document . getElementById ( 'ageMonths' ) ;
let age = ageInput && ageInput . value !== '' ? parseFloat ( ageInput . value ) : null ;
if ( age !== null ) {
if ( isNaN ( age ) ) age = null ;
if ( age !== null ) {
if ( age < 2 ) age = 2 ;
if ( age > 12 ) age = 12 ;
}
}
2026-01-28 15:51:28 +01:00
// Fred & Felia (MER-based) needs current weight to compute grams.
const needsWeight = this . foodSources . some ( fs => fs . chartType === 'mer' && ( fs . percentage || 0 ) > 0 ) ;
const weightInput = document . getElementById ( 'weight' ) ;
if ( needsWeight && weightInput ) {
const weightKg = this . getWeightInKg ( ) ;
if ( ! weightKg || weightKg < 0.1 ) {
this . showError ( 'weightError' , true ) ;
}
}
2025-11-12 18:00:36 +01:00
// Calculate per-food breakdown (chart-first)
2025-06-26 11:09:29 +02:00
const foodBreakdowns = [ ] ;
let totalDailyGrams = 0 ;
let hasValidFoods = false ;
2025-11-12 18:00:36 +01:00
// First pass: charted baseline
let chartedKcal = 0 ;
let chartedPercent = 0 ;
const firstPass = [ ] ;
2025-06-26 11:09:29 +02:00
this . foodSources . forEach ( fs => {
const energyPer100g = this . getFoodSourceEnergyPer100g ( fs ) ;
2025-11-12 18:00:36 +01:00
let chartGrams = null ;
2026-01-28 15:51:28 +01:00
if ( fs . chartType === 'mer' ) {
chartGrams = age !== null ? this . getKayaMerBasedGramsForFood ( age , energyPer100g ) : null ;
2025-11-12 18:00:36 +01:00
} else if ( fs . chartType === 'kibble' ) {
chartGrams = age !== null ? this . getKayaKibbleGramsForAge ( age ) : null ;
}
const gramsPortion = ( chartGrams !== null && chartGrams !== undefined ) ? ( chartGrams * ( fs . percentage || 0 ) / 100 ) : null ;
if ( gramsPortion !== null && energyPer100g && energyPer100g > 0 ) {
chartedKcal += gramsPortion * ( energyPer100g / 100 ) ;
chartedPercent += ( fs . percentage || 0 ) ;
}
firstPass . push ( { fs , energyPer100g , gramsPortion } ) ;
} ) ;
const kcalPerPercent = chartedPercent > 0 ? ( chartedKcal / chartedPercent ) : null ;
// Second pass: finalize amounts
2025-11-12 18:34:26 +01:00
let splitDailyTotal = 0 ;
let dailyOnlyTotal = 0 ;
2025-11-12 18:00:36 +01:00
firstPass . forEach ( ( { fs , energyPer100g , gramsPortion } ) => {
let dailyGramsForThisFood = 0 ;
let hasEnergyContent = ! ! ( energyPer100g && energyPer100g > 0 ) ;
2026-01-28 15:51:28 +01:00
if ( ( fs . chartType === 'mer' || fs . chartType === 'kibble' ) ) {
2025-11-12 18:00:36 +01:00
if ( gramsPortion !== null ) {
dailyGramsForThisFood = gramsPortion ;
}
} else {
if ( hasEnergyContent && kcalPerPercent && ( fs . percentage || 0 ) > 0 ) {
const perGramKcal = energyPer100g / 100 ;
const kcalForFood = ( fs . percentage || 0 ) * kcalPerPercent ;
dailyGramsForThisFood = kcalForFood / perGramKcal ;
2025-08-18 14:36:25 +02:00
} else {
2025-11-12 18:00:36 +01:00
hasEnergyContent = false ;
dailyGramsForThisFood = 0 ;
2025-08-18 14:36:25 +02:00
}
2025-06-26 11:09:29 +02:00
}
2025-11-12 18:00:36 +01:00
2025-11-12 18:34:26 +01:00
const isDailyOnly = fs . splitByMeals === false ;
const displayGrams = ( this . showPerMeal && ! isDailyOnly ) ? ( dailyGramsForThisFood / this . mealsPerDay ) : dailyGramsForThisFood ;
2025-11-12 18:00:36 +01:00
foodBreakdowns . push ( {
name : fs . name ,
percentage : fs . percentage ,
dailyGrams : dailyGramsForThisFood ,
2025-11-12 18:34:26 +01:00
isDailyOnly : isDailyOnly ,
2025-11-12 18:00:36 +01:00
displayGrams : displayGrams ,
dailyCups : null ,
displayCups : null ,
calories : hasEnergyContent ? ( dailyGramsForThisFood * ( energyPer100g / 100 ) ) : 0 ,
displayCalories : hasEnergyContent ? ( this . showPerMeal ? ( dailyGramsForThisFood * ( energyPer100g / 100 ) ) / this . mealsPerDay : ( dailyGramsForThisFood * ( energyPer100g / 100 ) ) ) : 0 ,
isLocked : fs . isLocked ,
hasEnergyContent : hasEnergyContent ,
foodSource : fs
} ) ;
totalDailyGrams += dailyGramsForThisFood ;
2025-11-12 18:34:26 +01:00
if ( isDailyOnly ) dailyOnlyTotal += dailyGramsForThisFood ; else splitDailyTotal += dailyGramsForThisFood ;
2025-11-12 18:00:36 +01:00
if ( dailyGramsForThisFood > 0 ) hasValidFoods = true ;
2025-06-26 11:09:29 +02:00
} ) ;
if ( ! hasValidFoods ) {
// Show errors for invalid food sources
this . foodSources . forEach ( fs => {
const energyInput = document . getElementById ( ` energy- ${ fs . id } ` ) ;
if ( energyInput && energyInput . value && ( ! this . getFoodSourceEnergyPer100g ( fs ) || this . getFoodSourceEnergyPer100g ( fs ) <= 0.1 ) ) {
this . showError ( ` energy-error- ${ fs . id } ` , true ) ;
}
} ) ;
dailyFoodResults . style . display = 'none' ;
if ( foodBreakdownResults ) foodBreakdownResults . style . display = 'none' ;
2025-08-18 12:45:44 +02:00
if ( feedingConfig ) feedingConfig . style . display = 'none' ;
2025-06-26 14:32:32 +02:00
2025-06-26 16:34:38 +02:00
// Hide unit buttons when no valid foods
const unitButtons = document . getElementById ( 'unitButtons' ) ;
if ( unitButtons ) unitButtons . style . display = 'none' ;
2025-11-12 18:34:26 +01:00
// If we have any foods with >0% but missing energy, show warnings only for those
const visibleBreakdownsMissing = foodBreakdowns . filter ( b => b . percentage > 0 ) ;
if ( visibleBreakdownsMissing . length > 0 ) {
2025-06-26 14:32:32 +02:00
// Show food amounts section with warnings for missing energy content
const unitLabel = unit === 'g' ? 'g' : unit === 'kg' ? 'kg' : unit === 'oz' ? 'oz' : 'lb' ;
2025-11-12 18:34:26 +01:00
const foodAmountsHTML = visibleBreakdownsMissing . map ( breakdown => {
2025-06-26 14:32:32 +02:00
const lockIndicator = breakdown . isLocked ? '<span class="dog-calculator-lock-indicator">🔒</span>' : '' ;
return `
<div class="dog-calculator-food-amount-item">
<div class="dog-calculator-food-amount-label">
<span> ${ breakdown . name } </span>
<span class="dog-calculator-food-percentage"> ${ breakdown . percentage } %</span>
${ lockIndicator }
</div>
<div class="dog-calculator-food-amount-value dog-calculator-warning" title="Enter energy content to calculate amount">
⚠️
</div>
</div>
` ;
} ) . join ( '' ) ;
if ( foodAmountsList ) {
foodAmountsList . innerHTML = foodAmountsHTML ;
}
if ( totalAmountDisplay ) {
totalAmountDisplay . textContent = "Enter energy content for all foods" ;
}
foodAmountsSection . style . display = 'block' ;
this . sendHeightToParent ( ) ;
} else {
foodAmountsSection . style . display = 'none' ;
}
2025-06-26 11:09:29 +02:00
return ;
}
2025-06-26 15:17:13 +02:00
// Update daily food results (total) - will be updated with proper units later
2025-06-08 22:27:21 +02:00
dailyFoodResults . style . display = 'block' ;
2025-06-26 16:34:38 +02:00
2025-08-18 12:45:44 +02:00
// Show feeding configuration when we have valid foods
2025-08-18 14:36:25 +02:00
if ( feedingConfig ) {
feedingConfig . style . display = 'block' ;
// Ensure "Per day" is checked when feeding config becomes visible
const showDaily = document . getElementById ( 'showDaily' ) ;
if ( showDaily && ! showDaily . checked && ! document . getElementById ( 'showPerMeal' ) . checked ) {
showDaily . checked = true ;
}
}
2025-08-18 12:45:44 +02:00
2025-06-26 16:34:38 +02:00
// Show unit buttons when daily results are shown
const unitButtons = document . getElementById ( 'unitButtons' ) ;
if ( unitButtons ) unitButtons . style . display = 'flex' ;
2025-06-26 11:09:29 +02:00
2025-11-12 18:34:26 +01:00
// Update per-food breakdown (show only items with >0%)
const visibleBreakdowns = foodBreakdowns . filter ( b => b . percentage > 0 ) ;
if ( foodBreakdownList && visibleBreakdowns . length > 0 ) {
const breakdownHTML = visibleBreakdowns . map ( breakdown => {
2025-08-18 14:36:25 +02:00
let valueContent ;
2025-11-12 18:34:26 +01:00
// Choose per-item frequency suffix: daily-only items stay /day even in per-meal view
const itemSuffix = ( this . showPerMeal && ! breakdown . isDailyOnly ) ? '/meal' : '/day' ;
2025-08-18 14:36:25 +02:00
if ( breakdown . hasEnergyContent ) {
if ( unit === 'cups' ) {
// For cups, use the pre-calculated cups value if available
if ( breakdown . displayCups !== null ) {
2025-11-12 18:34:26 +01:00
valueContent = ` ${ this . formatNumber ( breakdown . displayCups , decimals ) } ${ unitLabel } ${ itemSuffix } ` ;
2025-08-18 14:36:25 +02:00
} else {
valueContent = ` <span class="dog-calculator-warning" title="Cups only available for foods with kcal/cup measurement">N/A</span> ` ;
}
} else {
2025-08-18 15:33:44 +02:00
// For other units (g, kg, oz, lb)
2025-11-12 18:34:26 +01:00
valueContent = ` ${ this . formatNumber ( this . convertUnits ( breakdown . displayGrams , unit ) , decimals ) } ${ unitLabel } ${ itemSuffix } ` ;
2025-08-18 14:36:25 +02:00
}
} else {
valueContent = ` <span class="dog-calculator-warning" title="Enter energy content to calculate amount">⚠️</span> ` ;
}
2025-06-26 14:32:32 +02:00
return `
<div class="dog-calculator-food-result-item">
<span class="dog-calculator-food-result-label"> ${ breakdown . name } ( ${ breakdown . percentage } % ${ breakdown . isLocked ? ' - locked' : '' } ):</span>
<span class="dog-calculator-food-result-value"> ${ valueContent } </span>
</div>
` ;
} ) . join ( '' ) ;
2025-06-26 11:09:29 +02:00
foodBreakdownList . innerHTML = breakdownHTML ;
if ( foodBreakdownResults ) foodBreakdownResults . style . display = 'block' ;
} else {
if ( foodBreakdownResults ) foodBreakdownResults . style . display = 'none' ;
}
2025-06-26 12:29:35 +02:00
// Generate individual food amount breakdown
2025-06-08 22:27:21 +02:00
2025-06-26 15:17:13 +02:00
// Update daily food value with correct units
2025-11-12 18:34:26 +01:00
// When per-meal view is enabled, split-only items divide by meals/day; daily-only items remain as daily totals
const displayTotal = this . showPerMeal ? ( splitDailyTotal / this . mealsPerDay + dailyOnlyTotal ) : ( splitDailyTotal + dailyOnlyTotal ) ;
2025-08-18 14:36:25 +02:00
let convertedTotal ;
let totalDisplayText ;
if ( unit === 'cups' ) {
console . log ( 'Unit is cups, checking validity...' ) ;
// For cups, we can only show total if all foods with percentage > 0 have kcal/cup
const validForCups = foodBreakdowns . filter ( b => b . percentage > 0 )
. every ( b => b . displayCups !== null && b . displayCups !== undefined ) ;
console . log ( 'Valid for cups?' , validForCups , 'Breakdowns:' , foodBreakdowns ) ;
if ( validForCups ) {
// Calculate total cups using pre-calculated values
let totalCups = 0 ;
2025-08-18 15:33:44 +02:00
let totalCupsMin = 0 ;
let totalCupsMax = 0 ;
2025-08-18 14:36:25 +02:00
foodBreakdowns . forEach ( breakdown => {
if ( breakdown . percentage > 0 && breakdown . displayCups !== null ) {
totalCups += breakdown . displayCups ;
2025-08-18 15:33:44 +02:00
if ( breakdown . hasRange ) {
totalCupsMin += breakdown . displayCupsMin || breakdown . displayCups ;
totalCupsMax += breakdown . displayCupsMax || breakdown . displayCups ;
} else {
totalCupsMin += breakdown . displayCups ;
totalCupsMax += breakdown . displayCups ;
}
2025-08-18 14:36:25 +02:00
}
} ) ;
2025-08-18 15:33:44 +02:00
if ( hasRange && totalCupsMin !== totalCupsMax ) {
totalDisplayText = ` ${ this . formatNumber ( totalCupsMin , decimals ) } - ${ this . formatNumber ( totalCupsMax , decimals ) } ${ unitLabel } ${ frequencySuffix } ` ;
} else {
totalDisplayText = this . formatNumber ( totalCups , decimals ) + ` ${ unitLabel } ${ frequencySuffix } ` ;
}
2025-08-18 14:36:25 +02:00
} else {
totalDisplayText = 'Mixed units - see breakdown' ;
}
} else {
2025-08-18 15:33:44 +02:00
// Calculate totals for ranges
if ( hasRange ) {
let totalGramsMin = 0 ;
let totalGramsMax = 0 ;
foodBreakdowns . forEach ( breakdown => {
if ( breakdown . percentage > 0 && breakdown . hasEnergyContent ) {
totalGramsMin += breakdown . displayGramsMin || breakdown . displayGrams ;
totalGramsMax += breakdown . displayGramsMax || breakdown . displayGrams ;
}
} ) ;
const convertedMin = this . convertUnits ( totalGramsMin , unit ) ;
const convertedMax = this . convertUnits ( totalGramsMax , unit ) ;
if ( totalGramsMin !== totalGramsMax ) {
totalDisplayText = ` ${ this . formatNumber ( convertedMin , decimals ) } - ${ this . formatNumber ( convertedMax , decimals ) } ${ unitLabel } ${ frequencySuffix } ` ;
} else {
totalDisplayText = this . formatNumber ( convertedMin , decimals ) + ` ${ unitLabel } ${ frequencySuffix } ` ;
}
} else {
convertedTotal = this . convertUnits ( displayTotal , unit ) ;
totalDisplayText = this . formatNumber ( convertedTotal , decimals ) + ` ${ unitLabel } ${ frequencySuffix } ` ;
}
2025-08-18 14:36:25 +02:00
}
dailyFoodValue . textContent = totalDisplayText ;
2025-06-26 15:17:13 +02:00
2025-06-26 12:29:35 +02:00
// Build HTML for individual food amounts
2025-11-12 18:34:26 +01:00
const foodAmountsVisible = foodBreakdowns . filter ( b => b . percentage > 0 ) ;
const foodAmountsHTML = foodAmountsVisible . map ( breakdown => {
2025-06-26 12:29:35 +02:00
const lockIndicator = breakdown . isLocked ? '<span class="dog-calculator-lock-indicator">🔒</span>' : '' ;
2025-06-26 14:32:32 +02:00
if ( ! breakdown . hasEnergyContent ) {
// Show warning for food sources without energy content
return `
<div class="dog-calculator-food-amount-item">
<div class="dog-calculator-food-amount-label">
<span> ${ breakdown . name } </span>
<span class="dog-calculator-food-percentage"> ${ breakdown . percentage } %</span>
${ lockIndicator }
</div>
<div class="dog-calculator-food-amount-value dog-calculator-warning" title="Enter energy content to calculate amount">
⚠️
</div>
2025-06-26 12:29:35 +02:00
</div>
2025-06-26 14:32:32 +02:00
` ;
} else {
2025-08-18 12:45:44 +02:00
// For multi-day calculations: show total amount for all days
2025-08-18 14:36:25 +02:00
let amountDisplay ;
if ( unit === 'cups' ) {
// For cups, use pre-calculated cups value
if ( breakdown . dailyCups !== null ) {
const totalCupsForDays = breakdown . dailyCups * numDays ;
amountDisplay = ` ${ this . formatNumber ( totalCupsForDays , decimals ) } ${ unitLabel } ` ;
} else {
amountDisplay = ` <span class="dog-calculator-warning" title="Cups only available for foods with kcal/cup measurement">N/A</span> ` ;
}
} else {
// For other units, calculate from grams
const totalGramsForDays = this . showPerMeal
? ( breakdown . dailyGrams / this . mealsPerDay ) * numDays * this . mealsPerDay
: breakdown . dailyGrams * numDays ;
const convertedAmount = this . convertUnits ( totalGramsForDays , unit ) ;
amountDisplay = ` ${ this . formatNumber ( convertedAmount , decimals ) } ${ unitLabel } ` ;
}
2025-06-26 14:32:32 +02:00
return `
<div class="dog-calculator-food-amount-item">
<div class="dog-calculator-food-amount-label">
<span> ${ breakdown . name } </span>
<span class="dog-calculator-food-percentage"> ${ breakdown . percentage } %</span>
${ lockIndicator }
</div>
<div class="dog-calculator-food-amount-value">
2025-08-18 14:36:25 +02:00
${ amountDisplay }
2025-06-26 14:32:32 +02:00
</div>
2025-06-26 12:29:35 +02:00
</div>
2025-06-26 14:32:32 +02:00
` ;
}
2025-06-26 12:29:35 +02:00
} ) . join ( '' ) ;
// Calculate and display total
const totalFoodGrams = totalDailyGrams * numDays ;
// Update the display
if ( foodAmountsList ) {
foodAmountsList . innerHTML = foodAmountsHTML ;
}
if ( totalAmountDisplay ) {
2025-08-18 14:36:25 +02:00
if ( unit === 'cups' ) {
// For cups total, check if all foods can be converted
const validForCups = foodBreakdowns . filter ( b => b . percentage > 0 )
. every ( b => b . dailyCups !== null && b . dailyCups !== undefined ) ;
if ( validForCups ) {
// Calculate total cups using pre-calculated values
let totalCups = 0 ;
foodBreakdowns . forEach ( breakdown => {
if ( breakdown . percentage > 0 && breakdown . dailyCups !== null ) {
totalCups += breakdown . dailyCups * numDays ;
}
} ) ;
totalAmountDisplay . textContent = ` ${ this . formatNumber ( totalCups , decimals ) } ${ unitLabel } ` ;
} else {
totalAmountDisplay . textContent = 'Mixed units - see individual amounts' ;
}
} else {
const totalConverted = this . convertUnits ( totalFoodGrams , unit ) ;
totalAmountDisplay . textContent = ` ${ this . formatNumber ( totalConverted , decimals ) } ${ unitLabel } ` ;
}
2025-06-26 12:29:35 +02:00
}
2025-10-28 09:58:20 +01:00
foodAmountsSection . style . display = 'block' ;
this . sendHeightToParent ( ) ;
2025-06-08 22:27:21 +02:00
}
2025-06-26 11:09:29 +02:00
getFoodSourceEnergyPer100g ( foodSource ) {
if ( ! foodSource . energy || ! foodSource . energyUnit ) return null ;
const energy = parseFloat ( foodSource . energy ) ;
if ( isNaN ( energy ) ) return null ;
const unit = foodSource . energyUnit ;
// Convert all units to kcal/100g for internal calculations
switch ( unit ) {
case 'kcal100g' :
return energy ;
case 'kcalkg' :
return energy / 10 ; // 1 kg = 10 × 100g
case 'kcalcup' :
return energy / 1.2 ; // Assume 1 cup ≈ 120g for dry dog food
case 'kcalcan' :
return energy / 4.5 ; // Assume 1 can ≈ 450g for wet dog food
default :
return energy ;
}
}
2025-10-28 09:58:20 +01:00
// Iframe auto-resize for allowed embeddings
2025-06-08 22:27:21 +02:00
setupIframeResize ( ) {
2025-10-28 09:58:20 +01:00
// Only when embedded in an iframe
if ( window . top === window . self ) return ;
2025-06-08 22:27:21 +02:00
2025-10-28 09:58:20 +01:00
// Initial send once UI is ready
setTimeout ( ( ) => this . sendHeightToParent ( ) , 50 ) ;
// Monitor for content/attribute changes
2025-06-08 22:27:21 +02:00
const observer = new MutationObserver ( ( ) => {
2025-10-28 09:58:20 +01:00
clearTimeout ( this . _resizeTimer ) ;
this . _resizeTimer = setTimeout ( ( ) => this . sendHeightToParent ( ) , 100 ) ;
2025-06-08 22:27:21 +02:00
} ) ;
observer . observe ( document . body , {
childList : true ,
subtree : true ,
attributes : true
} ) ;
2025-10-28 09:58:20 +01:00
// On viewport resize
2025-06-08 22:27:21 +02:00
window . addEventListener ( 'resize' , ( ) => this . sendHeightToParent ( ) ) ;
}
sendHeightToParent ( ) {
2025-10-28 09:58:20 +01:00
if ( ! ( window . parent && window . parent !== window ) ) return ;
const container = document . getElementById ( 'dogCalculator' ) ;
// Prefer visual height including transform scaling
let height = 0 ;
if ( container ) {
const rect = container . getBoundingClientRect ( ) ;
height = Math . ceil ( rect . height ) ;
} else {
height = Math . max ( document . body . scrollHeight , document . documentElement . scrollHeight ) ;
2025-06-08 22:27:21 +02:00
}
2025-10-28 09:58:20 +01:00
window . parent . postMessage ( {
type : 'dogCalculatorResize' ,
height : height
} , '*' ) ;
2025-06-08 20:31:11 +02:00
}
2025-06-08 23:45:32 +02:00
// Modal functionality
showShareModal ( ) {
const modal = document . getElementById ( 'shareModal' ) ;
const shareUrl = document . getElementById ( 'shareUrl' ) ;
if ( modal && shareUrl ) {
shareUrl . value = window . location . href ;
2025-10-28 09:58:20 +01:00
// Use flex so content is centered within modal viewport
modal . style . display = 'flex' ;
// Sync modal scroll position with current page scroll so content is visible
try { modal . scrollTop = window . scrollY || document . documentElement . scrollTop || 0 ; } catch ( e ) { }
// Ensure the modal is visible even when the page is scrolled
// by recalculating parent iframe height (defensive)
this . sendHeightToParent ( ) ;
2025-06-08 23:45:32 +02:00
}
}
hideShareModal ( ) {
const modal = document . getElementById ( 'shareModal' ) ;
if ( modal ) modal . style . display = 'none' ;
2025-10-28 09:58:20 +01:00
this . sendHeightToParent ( ) ;
2025-06-08 23:45:32 +02:00
}
2025-10-28 09:58:20 +01:00
// Embed modal removed (embedding disabled)
2025-06-08 23:45:32 +02:00
2025-10-28 09:58:20 +01:00
// Embed modal removed (embedding disabled)
2025-06-08 23:45:32 +02:00
shareToFacebook ( ) {
const url = encodeURIComponent ( window . location . href ) ;
window . open ( 'https://www.facebook.com/sharer/sharer.php?u=' + url , '_blank' , 'width=600,height=400' ) ;
}
shareToTwitter ( ) {
const url = encodeURIComponent ( window . location . href ) ;
const text = encodeURIComponent ( 'Check out this useful dog calorie calculator!' ) ;
window . open ( 'https://twitter.com/intent/tweet?url=' + url + '&text=' + text , '_blank' , 'width=600,height=400' ) ;
}
shareToLinkedIn ( ) {
const url = encodeURIComponent ( window . location . href ) ;
window . open ( 'https://www.linkedin.com/sharing/share-offsite/?url=' + url , '_blank' , 'width=600,height=400' ) ;
}
shareViaEmail ( ) {
const subject = encodeURIComponent ( 'Dog Calorie Calculator' ) ;
const body = encodeURIComponent ( 'Check out this useful dog calorie calculator: ' + window . location . href ) ;
window . location . href = 'mailto:?subject=' + subject + '&body=' + body ;
}
async copyShareLink ( ) {
const shareUrl = document . getElementById ( 'shareUrl' ) ;
const copyBtn = document . getElementById ( 'shareCopy' ) ;
if ( shareUrl && copyBtn ) {
try {
await navigator . clipboard . writeText ( shareUrl . value ) ;
const originalText = copyBtn . textContent ;
copyBtn . textContent = 'Copied!' ;
copyBtn . classList . add ( 'copied' ) ;
setTimeout ( ( ) => {
copyBtn . textContent = originalText ;
copyBtn . classList . remove ( 'copied' ) ;
} , 2000 ) ;
} catch ( err ) {
// Fallback for older browsers
shareUrl . select ( ) ;
document . execCommand ( 'copy' ) ;
}
}
}
2025-10-28 09:58:20 +01:00
// Embed code copy removed (embedding disabled)
2025-06-08 20:31:11 +02:00
}
2025-06-08 22:27:21 +02:00
// Initialize calculator when DOM is ready
2025-06-08 20:31:11 +02:00
document . addEventListener ( 'DOMContentLoaded' , function ( ) {
2025-10-28 09:58:20 +01:00
// Allow embedding only from approved parent hosts
if ( window . top !== window . self ) {
const allowedHosts = [ 'caninenutritionandwellness.com' , 'www.caninenutritionandwellness.com' ] ;
let parentAllowed = false ;
// Prefer document.referrer when available
try {
if ( document . referrer ) {
const r = new URL ( document . referrer ) ;
parentAllowed = allowedHosts . includes ( r . hostname ) ;
}
} catch ( e ) { }
// Fallback: Chrome's ancestorOrigins (may be empty or absent)
if ( ! parentAllowed && window . location . ancestorOrigins && window . location . ancestorOrigins . length ) {
parentAllowed = Array . from ( window . location . ancestorOrigins ) . some ( ( originStr ) => {
try {
const o = new URL ( originStr ) ;
return allowedHosts . includes ( o . hostname ) ;
} catch ( e ) {
return false ;
}
} ) ;
}
if ( ! parentAllowed ) {
document . body . innerHTML = '<div style="max-width:720px;margin:40px auto;padding:16px;border:1px solid #ddd;border-radius:8px;font-family:system-ui, -apple-system, Segoe UI, Roboto, sans-serif;color:#333;line-height:1.5;text-align:center;">Embedding of this calculator is only allowed on caninenutritionandwellness.com.</div>' ;
return ;
}
}
2025-06-08 22:27:21 +02:00
new DogCalorieCalculator ( ) ;
2025-06-08 20:31:11 +02:00
} ) ;
< / script >
< / body >
< / html >