@@ -10,62 +10,120 @@
< link rel = "preconnect" href = "https://fonts.gstatic.com" crossorigin >
< link rel = "preconnect" href = "https://fonts.gstatic.com" crossorigin >
< link href = "https://fonts.googleapis.com/css2?family=Inter:wght@300..700&family=JetBrains+Mono:wght@400;600&display=swap" rel = "stylesheet" >
< link href = "https://fonts.googleapis.com/css2?family=Inter:wght@300..700&family=JetBrains+Mono:wght@400;600&display=swap" rel = "stylesheet" >
< style >
< style >
/* same styles as variant2 (kept compact) */
: root , [ data-theme = "light" ] {
: root , [ data-theme = "light" ] { --color-bg : #f7f6f2 ; --color-surface : #f9f8f5 ; --color-surface-2 : #fbfbf9 ; --color-surface-offset : #f3f0ec ; --color-divider : #dcd9d5 ; --color-border : #d4d1ca ; --color-text : #28251d ; --color-text-muted : #7a7974 ; --color-text-faint : #bab9b4 ; --color-primary : #01696f ; --color-primary-hover : #0c4e54 ; --color-warning : #964219 ; --color-success : #437a22 ; --shadow-sm : 0 1 px 2 px rgba ( 0 , 0 , 0 , 0.06 ) ; --shadow-md : 0 4 px 12 px rgba ( 0 , 0 , 0 , 0.08 ) ; --radius-md : .5 rem ; --radius-lg : .75 rem ; --radius-xl : 1 rem ; --text-xs : .75 rem ; --text-sm : 0.9 rem ; --text-base : 1 rem ; --space-3 : .75 rem ; --space-4 : 1 rem ; --space-6 : 1.5 rem ; --space-8 : 2 rem ; --font-body : 'Inter' , system-ui , sans-serif ; --font-mono : 'JetBrains Mono' , monospace }
--color-bg : #f7f6f2 ; --color-surface : #f9f8f5 ; --color-surface-2 : #fbfbf9 ;
[ data-theme = "dark" ] { --color-bg : #171614 ; --color-surface : #1c1b19 ; --color-text : #cdcc ca }
--color-surface-offset : #f3f0ec ; --color-divider : #dcd9d5 ; --color-border : #d4d1 ca ;
* { box-sizing : border-box ; margin : 0 ; padding : 0 } body { min-height : 100 dvh ; font-family : var ( - - font - body ) ; font-size : var ( - - text - base ) ; color : var ( - - color - text ) ; background : var ( - - color - bg ) ; display : flex ; flex-direction : column ; align-items : center ; padding : var ( - - space -8 ) var ( - - space -4 ) }
--color-text : #28251d ; --color-text-muted : #7a7974 ; --color-text-faint : #bab9b4 ;
--color-primary : #01696f ; --color-primary-hover : #0c4e54 ;
--color-warning : #964219 ; --color-success : #437a22 ;
--shadow-sm : 0 1 px 2 px oklch ( 0.2 0.01 80 / 0.06 ) ;
--shadow-md : 0 4 px 12 px oklch ( 0.2 0.01 80 / 0.08 ) ;
--shadow-lg : 0 12 px 32 px oklch ( 0.2 0.01 80 / 0.12 ) ;
--radius-sm : .375 rem ; --radius-md : .5 rem ; --radius-lg : .75 rem ; --radius-xl : 1 rem ; --radius-full : 9999 px ;
--text-xs : clamp ( .75 rem , .7 rem + .25 vw , .875 rem ) ;
--text-sm : clamp ( .875 rem , .8 rem + .35 vw , 1 rem ) ;
--text-base : clamp ( 1 rem , .95 rem + .25 vw , 1.125 rem ) ;
--text-lg : clamp ( 1.125 rem , 1 rem + .75 vw , 1.5 rem ) ;
--text-xl : clamp ( 1.5 rem , 1.2 rem + 1.25 vw , 2.25 rem ) ;
--space-1 : .25 rem ; --space-2 : .5 rem ; --space-3 : .75 rem ; --space-4 : 1 rem ;
--space-5 : 1.25 rem ; --space-6 : 1.5 rem ; --space-8 : 2 rem ;
--font-body : 'Inter' , 'Helvetica Neue' , sans-serif ;
--font-mono : 'JetBrains Mono' , 'Courier New' , monospace ;
--transition : 180 ms cubic-bezier ( .16 , 1 , .3 , 1 ) ;
}
[ data-theme = "dark" ] {
--color-bg : #171614 ; --color-surface : #1c1b19 ; --color-surface-2 : #201f1d ;
--color-surface-offset : #22211f ; --color-divider : #262523 ; --color-border : #393836 ;
--color-text : #cdccca ; --color-text-muted : #797876 ; --color-text-faint : #5a5957 ;
--color-primary : #4f98a3 ; --color-primary-hover : #227f8b ;
--color-warning : #bb653b ; --color-success : #6daa45 ;
--shadow-sm : 0 1 px 2 px oklch ( 0 0 0 / .2 ) ;
--shadow-md : 0 4 px 12 px oklch ( 0 0 0 / .3 ) ;
--shadow-lg : 0 12 px 32 px oklch ( 0 0 0 / .4 ) ;
}
* , * :: before , * :: after { box-sizing : border-box ; margin : 0 ; padding : 0 }
html { -webkit- font-smoothing : antialiased ; text-rendering : optimizeLegibility }
body { min-height : 100 dvh ; font-family : var ( - - font - body ) ; font-size : var ( - - text - base ) ; color : var ( - - color - text ) ; background : var ( - - color - bg ) ; display : flex ; flex-direction : column ; align-items : center ; padding : var ( - - space -8 ) var ( - - space -4 ) }
input , button { font : inherit ; color : inherit }
a , button , input { transition : color var ( - - transition ) , background var ( - - transition ) , border-color var ( - - transition ) , box-shadow var ( - - transition ) }
button { cursor : pointer ; background : none ; border : none }
. app-header { width : 100 % ; max-width : 720 px ; display : flex ; align-items : center ; justify-content : space-between ; margin-bottom : var ( - - space -8 ) }
. app-header { width : 100 % ; max-width : 720 px ; display : flex ; align-items : center ; justify-content : space-between ; margin-bottom : var ( - - space -8 ) }
. logo-text { font-size : 1.125 rem ; font-weight : 600 }
. logo { display : flex ; align-items : center ; gap : var ( - - space -3 ) }
. logo-sub { font-size : 0.85 rem ; color : var ( - - color - text - muted ) }
. logo-icon { color : var ( - - color - primary ) }
. theme-toggle { width : 36 px ; height : 36 px ; border-radius : .5 rem ; display : flex ; align-items : center ; justify-content : center ; border : 1 px solid rgba ( 0 , 0 , 0 , 0.06 ) ; background : transparent }
. logo-text { font-size : var ( - - text - lg ) ; font-weight : 600 ; color : var ( - - color - text ) ; letter-spacing : -.02 em }
. card { width : 100 % ; max-width : 720 px ; background : var ( - - color - surface ) ; border : 1 px solid var ( - - color - border ) ; border-radius : var ( - - radius - xl ) ; padding : var ( - - space -8 ) ; box-shadow : var ( - - shadow - md ) }
. logo-sub { font-size : var ( - - text - xs ) ; color : var ( - - color - text - muted ) ; margin-top : 1 px }
. form-row { display : grid ; grid-template-columns : 1 fr 1 fr auto ; gap : 1 rem ; align-items : end ; margin-bottom : 1.5 rem }
. theme-toggle { width : 36 px ; height : 36 px ; border-radius : var ( - - radius - md ) ; display : flex ; align-items : center ; justify-content : center ; color : var ( - - color - text - muted ) ; border : 1 px solid oklch ( from var ( - - color - text ) l c h / .1 ) }
. form-field { display : flex ; flex-direction : column ; gap : .5 rem }
. theme-toggle : hover { color : var ( - - color - text ) ; background : var ( - - color - surface ) }
. form-label { font-size : .75 rem ; color : var ( - - color - text - muted ) ; text-transform : uppercase }
. card { width : 100 % ; max-width : 720 px ; background : var ( - - color - surface ) ; border : 1 px solid oklch ( from var ( - - color - text ) l c h / .08 ) ; border-radius : var ( - - radius - xl ) ; padding : var ( - - space -8 ) ; box-shadow : var ( - - shadow - md ) }
. form-input { padding : .75 rem 1 rem ; background : var ( - - color - bg ) ; border : 1 px solid var ( - - color - border ) ; border-radius : .5 rem }
. form-row { display : grid ; grid-template-columns : 1 fr 1 fr auto ; gap : var ( - - space -4 ) ; align-items : end ; margin-bottom : var ( - - space -6 ) }
. input-prefix { position : absolute ; left : 1 rem ; top : 50 % ; transform : translateY ( -50 % ) ; color : var ( - - color - text - muted ) }
@ media ( max-width : 600px ) { . form-row { grid-template-columns : 1 fr } }
. btn-start { padding : .6 rem 1.2 rem ; background : var ( - - color - primary ) ; color : #fff ; border-radius : .5 rem ; display : flex ; align-items : center ; gap : .5 rem }
. form-field { display : flex ; flex-direction : column ; gap : var ( - - space -2 ) }
. btn-reset { padding : .4 rem .75 rem ; border : 1 px solid var ( - - color - border ) ; border-radius : .5 rem }
. form-label { font-size : var ( - - text - xs ) ; font-weight : 600 ; color : var ( - - color - text - muted ) ; text-transform : uppercase ; letter-spacing : .06 em }
. divider { height : 1 px ; background : var ( - - color - divider ) ; margin : 1.25 rem 0 }
. form-input { padding : var ( - - space -3 ) var ( - - space -4 ) ; background : var ( - - color - bg ) ; border : 1 px solid var ( - - color - border ) ; border-radius : var ( - - radius - md ) ; font-size : var ( - - text - base ) ; color : var ( - - color - text ) ; outline : none ; width : 100 % }
. status-badge { display : inline-flex ; align-items : center ; gap : .5 rem ; padding : .25 rem .75 rem ; border-radius : 9999 px ; background : var ( - - color - surface - offset ) ; color : var ( - - color - text - muted ) ; border : 1 px solid var ( - - color - border ) }
. form-input : focus { border-color : var ( - - color - primary ) ; box-shadow : 0 0 0 3 px oklch ( from var ( - - color - primary ) l c h / .15 ) }
. running-indicator { width : 10 px ; height : 10 px ; border-radius : 9999 px ; background : currentColor }
. form-input : disabled { opacity : .5 ; cursor : not-allowed }
. running-indicator . active { animation : pulse 1.2 s infinite } @ keyframes pulse { 0 % { opacity : 1 } 50 % { opacity : .3 } 100 % { opacity : 1 } }
. form-hint { font-size : var ( - - text - xs ) ; color : var ( - - color - text - faint ) ; margin-top : var ( - - space -1 ) }
. cost-display-wrap { position : relative ; background : var ( - - color - surface - offset ) ; border : 1 px solid var ( - - color - border ) ; border-radius : 1 rem ; padding : 2 rem ; text-align : center ; min-height : 160 px ; display : flex ; flex-direction : column ; align-items : center ; justify-content : center }
. input-prefix-wrap { position : relative }
. cost-display-amount { font-family : var ( - - font - mono ) ; font-size : clamp ( 2.5 rem , 6 vw , 4.5 rem ) ; font-weight : 700 }
. input-prefix-wrap . form-input { padding-left : var ( - - space -8 ) }
. info-grid { display : grid ; grid-template-columns : repeat ( auto - fit , minmax ( 160 px , 1 fr ) ) ; gap : 1 rem ; margin-top : 1.5 rem }
. input-prefix { position : absolute ; left : var ( - - space -3 ) ; top : 50 % ; transform : translateY ( -50 % ) ; color : var ( - - color - text - muted ) ; font-size : var ( - - text - sm ) ; pointer-events : none ; line-height : 1 }
. info-tile { background : var ( - - color - surface -2 ) ; border : 1 px solid var ( - - color - border ) ; border-radius : .5 rem ; padding : 1 rem }
. btn-start { padding : var ( - - space -3 ) var ( - - space -6 ) ; background : var ( - - color - primary ) ; color : #fff ; border-radius : var ( - - radius - md ) ; font-size : var ( - - text - sm ) ; font-weight : 600 ; white-space : nowrap ; display : flex ; align-items : center ; gap : var ( - - space -2 ) ; box-shadow : var ( - - shadow - sm ) ; height : 44 px }
. info-tile-label { font-size : .75 rem ; color : var ( - - color - text - muted ) ; text-transform : uppercase ; margin-bottom : .5 rem }
. btn-start : hover { background : var ( - - color - primary - hover ) ; box-shadow : var ( - - shadow - md ) }
. info-tile-value { font-family : var ( - - font - mono ) ; font-weight : 600 }
. btn-start : active { transform : scale ( .98 ) }
. btn-reset { padding : var ( - - space -2 ) var ( - - space -4 ) ; border : 1 px solid var ( - - color - border ) ; border-radius : var ( - - radius - md ) ; font-size : var ( - - text - xs ) ; color : var ( - - color - text - muted ) ; font-weight : 500 ; display : flex ; align-items : center ; gap : var ( - - space -2 ) }
. btn-reset : hover { color : var ( - - color - text ) ; border-color : var ( - - color - text - muted ) }
. divider { height : 1 px ; background : var ( - - color - divider ) ; margin : var ( - - space -6 ) 0 }
. action-row { display : flex ; align-items : center ; justify-content : space-between ; flex-wrap : wrap ; gap : var ( - - space -3 ) ; margin-bottom : var ( - - space -6 ) }
. status-badge { display : inline-flex ; align-items : center ; gap : var ( - - space -2 ) ; padding : var ( - - space -1 ) var ( - - space -3 ) ; border-radius : var ( - - radius - full ) ; font-size : var ( - - text - xs ) ; font-weight : 600 ; letter-spacing : .04 em ; text-transform : uppercase ; background : var ( - - color - surface - offset ) ; color : var ( - - color - text - muted ) ; border : 1 px solid var ( - - color - border ) }
. status-badge . running { background : color-mix ( in oklch , var ( - - color - warning ) 12 % , var ( - - color - surface ) ) ; color : var ( - - color - warning ) ; border-color : color-mix ( in oklch , var ( - - color - warning ) 30 % , var ( - - color - border ) ) }
. status-badge . stopped { background : color-mix ( in oklch , var ( - - color - success ) 10 % , var ( - - color - surface ) ) ; color : var ( - - color - success ) ; border-color : color-mix ( in oklch , var ( - - color - success ) 30 % , var ( - - color - border ) ) }
. running-indicator { width : 10 px ; height : 10 px ; border-radius : var ( - - radius - full ) ; background : currentColor ; display : inline-block }
@ keyframes pulse { 0 % { opacity : 1 } 50 % { opacity : .3 } 100 % { opacity : 1 } }
. running-indicator . active { animation : pulse 1.2 s ease-in-out infinite }
. cost-display-wrap { position : relative ; background : var ( - - color - surface - offset ) ; border : 1 px solid oklch ( from var ( - - color - text ) l c h / .06 ) ; border-radius : var ( - - radius - lg ) ; padding : var ( - - space -8 ) var ( - - space -8 ) var ( - - space -6 ) ; text-align : center ; min-height : 160 px ; display : flex ; flex-direction : column ; align-items : center ; justify-content : center }
. cost-display-label { font-size : var ( - - text - xs ) ; color : var ( - - color - text - muted ) ; text-transform : uppercase ; letter-spacing : .08 em ; font-weight : 600 ; margin-bottom : var ( - - space -3 ) }
. cost-display-amount { font-family : var ( - - font - mono ) ; font-size : clamp ( 2.5 rem , 6 vw , 4.5 rem ) ; font-weight : 600 ; letter-spacing : -.03 em ; color : var ( - - color - text ) ; line-height : 1 ; transition : color .3 s ease }
. cost-display-amount . running { color : var ( - - color - warning ) }
. cost-display-currency { font-size : var ( - - text - xl ) ; font-weight : 400 ; color : var ( - - color - text - muted ) ; vertical-align : super ; margin-right : var ( - - space -2 ) ; font-family : var ( - - font - mono ) }
. cost-display-elapsed { margin-top : var ( - - space -3 ) ; font-size : var ( - - text - xs ) ; color : var ( - - color - text - faint ) ; font-family : var ( - - font - mono ) }
. info-grid { display : grid ; grid-template-columns : repeat ( auto - fit , minmax ( 160 px , 1 fr ) ) ; gap : var ( - - space -3 ) ; margin-top : var ( - - space -6 ) }
. info-tile { background : var ( - - color - surface -2 , var ( - - color - surface ) ) ; border : 1 px solid oklch ( from var ( - - color - text ) l c h / .06 ) ; border-radius : var ( - - radius - md ) ; padding : var ( - - space -4 ) }
. info-tile-label { font-size : var ( - - text - xs ) ; color : var ( - - color - text - muted ) ; text-transform : uppercase ; letter-spacing : .05 em ; font-weight : 600 ; margin-bottom : var ( - - space -2 ) }
. info-tile-value { font-family : var ( - - font - mono ) ; font-size : var ( - - text - sm ) ; font-weight : 600 ; color : var ( - - color - text ) }
< / style >
< / style >
< / head >
< / head >
< body >
< body >
< header class = "app-header" >
< header class = "app-header" >
< div style = "display:flex;align-items:center;gap:1rem " >
< div class = "logo " >
< svg width = "32" height = "32" viewBox = "0 0 32 32" fill = "none" aria-hidden = "true" > < circle cx = "16" cy = "16" r = "14" stroke = "currentColor" stroke-width = "2" / > < path d = "M16 8v8l5 3" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" / > < circle cx = "16" cy = "16" r = "2" fill = "currentColor" / > < / svg >
< svg class = "logo-icon" width = "32" height = "32" viewBox = "0 0 32 32" fill = "none" aria-label = "Meeting Cost Tracker" >
< circle cx = "16" cy = "16" r = "14" stroke = "currentColor" stroke-width = "2" / >
< path d = "M16 8v8l5 3" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" / >
< circle cx = "16" cy = "16" r = "2" fill = "currentColor" / >
< / svg >
< div >
< div >
< div class = "logo-text" > Meeting Cost Tracker< / div >
< div class = "logo-text" > Meeting Cost Tracker< / div >
< div class = "logo-sub" > Live meeting cost overview< / div >
< div class = "logo-sub" > Live meeting cost overview< / div >
< / div >
< / div >
< / div >
< / div >
< button class = "theme-toggle" data-theme-toggle aria-label = "Toggle theme" >
< button class = "theme-toggle" data-theme-toggle aria-label = "Toggle theme" >
< svg width = "18" height = "18" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < circle cx = "12" cy = "12" r = "5" / > < / svg >
< svg width = "18" height = "18" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< circle cx = "12" cy = "12" r = "5" / > < path d = "M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" / >
< / svg >
< / button >
< / button >
< / header >
< / header >
< main class = "card" >
< main class = "card" >
< div class = "form-row" >
< div class = "form-row" >
< div class = "form-field" >
< div class = "form-field" >
< label class = "form-label" for = "devCount" > Developers present< / label >
< label class = "form-label" for = "devCount" > Developers present< / label >
< input class = "form-input" type = "number" id = "devCount" min = "1" max = "500" value = "5" aria-label = "Developers present" >
< input class = "form-input" type = "number" id = "devCount" min = "1" max = "500" value = "5" >
< div style = "font-size:.85rem;color:var(--color-text-muted) " > n umber of participants< / div >
< div class = "form-hint " > N umber of participants< / div >
< / div >
< / div >
< div class = "form-field" >
< div class = "form-field" >
< label class = "form-label" for = "avgSalary" > Avg. monthly gross salary< / label >
< label class = "form-label" for = "avgSalary" > Avg. monthly gross salary< / label >
< div style = "position:relative " >
< div class = "input-prefix-wrap " >
< span class = "input-prefix" > EUR< / span >
< span class = "input-prefix" > EUR< / span >
< input class = "form-input" type = "number" id = "avgSalary" min = "1000" max = "50000" value = "5000" style = "padding-left:3.5rem" aria-label = "Average monthly salary" >
< input class = "form-input" type = "number" id = "avgSalary" min = "1000" max = "50000" value = "5000" style = "padding-left:3.5rem" >
< / div >
< / div >
< div style = "font-size:.85rem;color:var(--color-text-muted) " > b rutto, ava rage salary< / div >
< div class = "form-hint " > B rutto, ave rage salary< / div >
< / div >
< / div >
< button class = "btn-start" id = "mainBtn" onclick = "handleBtn()" >
< button class = "btn-start" id = "mainBtn" onclick = "handleBtn()" >
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "currentColor" id = "btnIcon" > < polygon points = "5,3 19,12 5,21" / > < / svg >
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "currentColor" id = "btnIcon" > < polygon points = "5,3 19,12 5,21" / > < / svg >
@@ -73,43 +131,77 @@
< / button >
< / button >
< / div >
< / div >
< div class = "divider" > < / div >
< div class = "divider" > < / div >
< div class = "action-row" >
< div style = "display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem " >
< span class = "status-badge" id = "statusBadge ">
< span class = "status-badge" id = "statusBadge" > < span class = " running-indicator" id = "runIndicator" > < / span > Ready < / span >
< span class = "running-indicator" id = "runIndicator" > < / span >
< button class = "btn-reset" id = "resetBtn" onclick = "resetTimer()" style = "display:none" > Reset< / button >
Ready
< / span >
< button class = "btn-reset" id = "resetBtn" onclick = "resetTimer()" style = "display:none" >
Reset
< / button >
< / div >
< / div >
< div class = "cost-display-wrap" >
< div class = "cost-display-wrap" >
< div style = "font-size:.75rem;color:var(--color-text-muted);text-transform:uppercase;margin-bottom:.5rem " > Total cost so far< / div >
< div class = "cost-display-label " > Total cost so far< / div >
< div class = "cost-display-amount" id = "costDisplay" > < span style = "font-family:var(--font-mono);font-size:1rem;margin-right:.4rem" > EUR < / span > < span id = "costValue" > 0.00 < / span > < / div >
< div class = "cost-display-amount" id = "costDisplay" >
< div style = "margin-top:.5rem;font-size:.85rem;color:var(--color-text-muted)" id = "elapsedDisplay" > Not started < / div >
< span class = "cost-display-currency" > EUR < / span > < span id = "costValue" > 0.00 < / span >
< / div >
< div class = "cost-display-elapsed" id = "elapsedDisplay" > Not started< / div >
< / div >
< / div >
< div class = "info-grid" id = "infoGrid" style = "display:none" >
< div class = "info-grid" id = "infoGrid" style = "display:none" >
< div class = "info-tile" > < div class = "info-tile-label" > Cost / second < / div > < div class = "info-tile-value" id = "tilePerSec" > - < / div > < / div >
< div class = "info-tile" >
< div class = "info-tile" > < div class = "info-tile-label" > Cost / minute < / div > < div class = "info-tile-value" id = "tilePerMin" > - < / div > < / div >
< div class = "info-tile-label" > Cost / second < / div >
< div class = "info-tile" > < div class = "info-tile-label" > Cost / hour < / div > < div class = "info-tile -value" id = "tilePerHour " > -< / div > < / div >
< div class = "info-tile-value" id = "tilePerSec " > -< / div >
< div class = "info-tile" > < div class = "info-tile-label" > People × salary < / div > < div class = "info-tile-value" id = "tileTotal" > - < / div > < / div >
< / div >
< div class = "info-tile" > < div class = "info-tile-label" > Annual salary (per person) < / div > < div class = "info-tile-value" id = "tileAnnual" > - < / div > < / div >
< div class = "info-tile" >
< div class = "info-tile" > < div class = "info-tile-label" > incl. employer contributions (× 1.2) < / div > < div class = "info-tile-value" id = "tileEmployer" > - < / div > < / div >
< div class = "info-tile-label" > Cost / minute < / div >
< div class = "info-tile" > < div class = "info-tile-label" > Total annual cost < / div > < div class = "info-tile-value" id = "tileAnnualTotal" > - < / div > < / div >
< div class = "info-tile-value" id = "tilePerMin" > - < / div >
< / div >
< div class = "info-tile" >
< div class = "info-tile-label" > Cost / hour< / div >
< div class = "info-tile-value" id = "tilePerHour" > -< / div >
< / div >
< div class = "info-tile" >
< div class = "info-tile-label" > People x salary< / div >
< div class = "info-tile-value" id = "tileTotal" > -< / div >
< / div >
< div class = "info-tile" >
< div class = "info-tile-label" > Annual salary (per person)< / div >
< div class = "info-tile-value" id = "tileAnnual" > -< / div >
< / div >
< div class = "info-tile" >
< div class = "info-tile-label" > incl. employer contributions (x1.2)< / div >
< div class = "info-tile-value" id = "tileEmployer" > -< / div >
< / div >
< div class = "info-tile" >
< div class = "info-tile-label" > Total annual cost< / div >
< div class = "info-tile-value" id = "tileAnnualTotal" > -< / div >
< / div >
< / div >
< / div >
< / main >
< / main >
< script >
< script >
( function ( ) {
( function ( ) {
var root = document . documentElement ; var dark = false ; var btn = document . querySelector ( '[data-theme-toggle]' ) ;
var root = document . documentElement ;
var sunSvg = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/></svg>' ;
var dark = false ;
var btn = document . querySelector ( '[data-theme-toggle]' ) ;
var sunSvg = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>' ;
var moonSvg = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>' ;
var moonSvg = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>' ;
if ( btn ) { root . setAttribute ( 'data-theme' , dark ? 'dark' : 'light' ) ; btn . innerHTML = dark ? sunSvg : moonSvg ; btn . addEventListener ( 'click' , function ( ) { dark = ! dark ; root . setAttribute ( 'data-theme' , dark ? 'dark' : 'light' ) ; btn . innerHTML = dark ? sunSvg : moonSvg ; } ) ; }
if ( btn ) {
root . setAttribute ( 'data-theme' , dark ? 'dark' : 'light' ) ;
btn . innerHTML = dark ? sunSvg : moonSvg ;
btn . addEventListener ( 'click' , function ( ) {
dark = ! dark ; root . setAttribute ( 'data-theme' , dark ? 'dark' : 'light' ) ; btn . innerHTML = dark ? sunSvg : moonSvg ;
} ) ;
}
} ) ( ) ;
} ) ( ) ;
var timer = null , isRunning = false , startTime = null , pausedElapsed = 0 ;
var timer = null , isRunning = false , startTime = null , pausedElapsed = 0 ;
// English formatting (GBP/GB style but currency EUR)
function fmt ( n ) { return new Intl . NumberFormat ( 'en-GB' , { style : 'currency' , currency : 'EUR' , minimumFractionDigits : 2 , maximumFractionDigits : 2 } ) . format ( n ) ; }
function fmt ( n ) { return new Intl . NumberFormat ( 'en-GB' , { style : 'currency' , currency : 'EUR' , minimumFractionDigits : 2 , maximumFractionDigits : 2 } ) . format ( n ) ; }
function fmtPlain ( n , d ) { return n . toLocaleString ( 'en-GB' , { minimumFractionDigits : d , maximumFractionDigits : d } ) ; }
function fmtPlain ( n , d ) { return n . toLocaleString ( 'en-GB' , { minimumFractionDigits : d , maximumFractionDigits : d } ) ; }
function fmtElapsed ( sec ) { var h = Math . floor ( sec / 3600 ) , m = Math . floor ( ( sec % 3600 ) / 60 ) , s = Math . floor ( sec % 60 ) ; if ( h > 0 ) return h + 'h ' + String ( m ) . padStart ( 2 , '0' ) + 'm ' + String ( s ) . padStart ( 2 , '0' ) + 's' ; if ( m > 0 ) return m + 'm ' + String ( s ) . padStart ( 2 , '0' ) + 's' ; return Math . floor ( sec ) + 's elapsed' ; }
function fmtElapsed ( sec ) {
var h = Math . floor ( sec / 3600 ) , m = Math . floor ( ( sec % 3600 ) / 60 ) , s = Math . floor ( sec % 60 ) ;
if ( h > 0 ) return h + 'h ' + String ( m ) . padStart ( 2 , '0' ) + 'm ' + String ( s ) . padStart ( 2 , '0' ) + 's' ;
if ( m > 0 ) return m + 'm ' + String ( s ) . padStart ( 2 , '0' ) + 's' ;
return Math . floor ( sec ) + 's elapsed' ;
}
function getParams ( ) {
function getParams ( ) {
var devCount = Math . max ( 1 , parseInt ( document . getElementById ( 'devCount' ) . value ) || 1 ) ;
var devCount = Math . max ( 1 , parseInt ( document . getElementById ( 'devCount' ) . value ) || 1 ) ;
var avgSalary = Math . max ( 0 , parseFloat ( document . getElementById ( 'avgSalary' ) . value ) || 5000 ) ;
var avgSalary = Math . max ( 0 , parseFloat ( document . getElementById ( 'avgSalary' ) . value ) || 5000 ) ;
@@ -119,7 +211,6 @@
var costPerSecond = totalCost / 220 / 8 / 60 / 60 ;
var costPerSecond = totalCost / 220 / 8 / 60 / 60 ;
return { devCount : devCount , avgSalary : avgSalary , annualSalary : annualSalary , annualWithEmployer : annualWithEmployer , totalCost : totalCost , costPerSecond : costPerSecond } ;
return { devCount : devCount , avgSalary : avgSalary , annualSalary : annualSalary , annualWithEmployer : annualWithEmployer , totalCost : totalCost , costPerSecond : costPerSecond } ;
}
}
function tick ( ) {
function tick ( ) {
var elapsed = pausedElapsed + ( Date . now ( ) - startTime ) / 1000 ;
var elapsed = pausedElapsed + ( Date . now ( ) - startTime ) / 1000 ;
var p = getParams ( ) ;
var p = getParams ( ) ;
@@ -130,37 +221,52 @@
document . getElementById ( 'tilePerMin' ) . textContent = 'EUR ' + fmtPlain ( p . costPerSecond * 60 , 2 ) ;
document . getElementById ( 'tilePerMin' ) . textContent = 'EUR ' + fmtPlain ( p . costPerSecond * 60 , 2 ) ;
document . getElementById ( 'tilePerHour' ) . textContent = 'EUR ' + fmtPlain ( p . costPerSecond * 3600 , 2 ) ;
document . getElementById ( 'tilePerHour' ) . textContent = 'EUR ' + fmtPlain ( p . costPerSecond * 3600 , 2 ) ;
document . getElementById ( 'tileTotal' ) . textContent = p . devCount + ' x EUR ' + fmtPlain ( p . avgSalary , 2 ) ;
document . getElementById ( 'tileTotal' ) . textContent = p . devCount + ' x EUR ' + fmtPlain ( p . avgSalary , 2 ) ;
// annual / employer / total
var annual = p . annualSalary ;
document . getElementById ( 'tileAnnual' ) . textContent = fmtPlain ( p . annualSalary , 0 ) + ' €' ;
var employer = p . annualWithEmployer ;
document . getElementById ( 'tileEmployer' ) . textContent = fmtPlain ( p . annualWithEmployer , 0 ) + ' €' ;
var annualTotal = p . totalCost ;
document . getElementById ( 'tileAnnualTotal ' ) . textContent = fmtPlain ( p . totalCost , 0 ) + ' € ' ;
document . getElementById ( 'tileAnnual' ) . textContent = fmtPlain ( annual , 0 ) + ' EUR ' ;
document . getElementById ( 'tileEmployer' ) . textContent = fmtPlain ( employer , 0 ) + ' EUR' ;
document . getElementById ( 'tileAnnualTotal' ) . textContent = fmtPlain ( annualTotal , 0 ) + ' EUR' ;
}
}
function handleBtn ( ) { isRunning ? pauseTimer ( ) : startTimer ( ) ; }
function handleBtn ( ) { isRunning ? pauseTimer ( ) : startTimer ( ) ; }
function startTimer ( ) {
function startTimer ( ) {
isRunning = true ; startTime = Date . now ( ) ; timer = setInterval ( tick , 100 ) ;
isRunning = true ; startTime = Date . now ( ) ;
timer = setInterval ( tick , 100 ) ;
document . getElementById ( 'btnLabel' ) . textContent = 'Pause' ;
document . getElementById ( 'btnLabel' ) . textContent = 'Pause' ;
document . getElementById ( 'btnIcon' ) . innerHTML = '<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>' ;
document . getElementById ( 'btnIcon' ) . innerHTML = '<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>' ;
document . getElementById ( 'costDisplay' ) . classList . add ( 'running' ) ;
document . getElementById ( 'costDisplay' ) . classList . add ( 'running' ) ;
document . getElementById ( 'statusBadge' ) . className = 'status-badge running' ;
document . getElementById ( 'statusBadge' ) . className = 'status-badge running' ;
document . getElementById ( 'statusBadge' ) . innerHTML = '<span class="running-indicator active"></span> Running' ;
document . getElementById ( 'statusBadge' ) . innerHTML = '<span class="running-indicator active"></span> Running' ;
document . getElementById ( 'resetBtn' ) . style . display = 'inline- flex' ;
document . getElementById ( 'resetBtn' ) . style . display = 'flex' ;
document . getElementById ( 'infoGrid' ) . style . display = 'grid' ;
document . getElementById ( 'infoGrid' ) . style . display = 'grid' ;
document . getElementById ( 'devCount' ) . disabled = true ; document . getElementById ( 'avgSalary' ) . disabled = true ;
document . getElementById ( 'devCount' ) . disabled = true ;
document . getElementById ( 'avgSalary' ) . disabled = true ;
tick ( ) ;
tick ( ) ;
}
}
function pauseTimer ( ) {
function pauseTimer ( ) {
isRunning = false ; pausedElapsed += ( Date . now ( ) - startTime ) / 1000 ; clearInterval ( timer ) ; timer = null ;
isRunning = false ; pausedElapsed += ( Date . now ( ) - startTime ) / 1000 ;
clearInterval ( timer ) ; timer = null ;
document . getElementById ( 'btnLabel' ) . textContent = 'Resume' ;
document . getElementById ( 'btnLabel' ) . textContent = 'Resume' ;
document . getElementById ( 'btnIcon' ) . innerHTML = '<polygon points="5,3 19,12 5,21"/>' ;
document . getElementById ( 'btnIcon' ) . innerHTML = '<polygon points="5,3 19,12 5,21"/>' ;
document . getElementById ( 'costDisplay' ) . classList . remove ( 'running' ) ;
document . getElementById ( 'costDisplay' ) . classList . remove ( 'running' ) ;
document . getElementById ( 'statusBadge' ) . className = 'status-badge stopped' ;
document . getElementById ( 'statusBadge' ) . className = 'status-badge stopped' ;
document . getElementById ( 'statusBadge' ) . innerHTML = '<span class="running-indicator"></span> Paused' ;
document . getElementById ( 'statusBadge' ) . innerHTML = '<span class="running-indicator"></span> Paused' ;
}
}
function resetTimer ( ) {
function resetTimer ( ) { if ( timer ) clearInterval ( timer ) ; timer = null ; isRunning = false ; pausedElapsed = 0 ; startTime = null ; document . getElementById ( 'costValue' ) . textContent = '0.00' ; document . getElementById ( 'elapsedDisplay' ) . textContent = 'Not started' ; document . getElementById ( 'btnLabel' ) . textContent = 'Start' ; document . getElementById ( 'btnIcon' ) . innerHTML = '<polygon points="5,3 19,12 5,21"/>' ; document . getElementById ( 'costDisplay' ) . classList . remove ( 'running' ) ; document . getElementById ( 'statusBadge' ) . className = 'status-badge' ; document . getElementById ( 'statusBadge' ) . innerHTML = '<span class="running-indicator"></span> Ready' ; document . getElementById ( 'resetBtn' ) . style . display = 'none' ; document . getElementById ( 'infoGrid' ) . style . display = 'none' ; document . getElementById ( 'devCount' ) . disabled = false ; document . getElementById ( 'avgSalary' ) . disabled = false ; }
if ( timer ) clearInterval ( timer ) ;
timer = null ; isRunning = false ; pausedElapsed = 0 ; startTime = null ;
document . getElementById ( 'costValue' ) . textContent = '0.00' ;
document . getElementById ( 'elapsedDisplay' ) . textContent = 'Not started' ;
document . getElementById ( 'btnLabel' ) . textContent = 'Start' ;
document . getElementById ( 'btnIcon' ) . innerHTML = '<polygon points="5,3 19,12 5,21"/>' ;
document . getElementById ( 'costDisplay' ) . classList . remove ( 'running' ) ;
document . getElementById ( 'statusBadge' ) . className = 'status-badge' ;
document . getElementById ( 'statusBadge' ) . innerHTML = '<span class="running-indicator"></span> Ready' ;
document . getElementById ( 'resetBtn' ) . style . display = 'none' ;
document . getElementById ( 'infoGrid' ) . style . display = 'none' ;
document . getElementById ( 'devCount' ) . disabled = false ;
document . getElementById ( 'avgSalary' ) . disabled = false ;
}
// input listeners: update per-second display when editing
// input listeners: update per-second display when editing
document . getElementById ( 'devCount' ) . addEventListener ( 'input' , function ( ) { if ( ! isRunning ) { var p = getParams ( ) ; if ( p . costPerSecond > 0 ) document . getElementById ( 'tilePerSec' ) . textContent = 'EUR ' + p . costPerSecond . toLocaleString ( 'en-GB' , { minimumFractionDigits : 4 , maximumFractionDigits : 4 } ) ; } } ) ;
document . getElementById ( 'devCount' ) . addEventListener ( 'input' , function ( ) { if ( ! isRunning ) { var p = getParams ( ) ; if ( p . costPerSecond > 0 ) document . getElementById ( 'tilePerSec' ) . textContent = 'EUR ' + p . costPerSecond . toLocaleString ( 'en-GB' , { minimumFractionDigits : 4 , maximumFractionDigits : 4 } ) ; } } ) ;