From c65e5611e6d29132975e09ede76ea1f81df012b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 23:09:51 +0000 Subject: [PATCH] v1.59: Solar+? says when a curtailable inverter is needed, and shows the numbers to check - Where the plan's feed-in price goes below zero (Amber) and the panels are assumed to switch off then, "what to ask installers for" asks for a "curtailable" inverter, and the recommendation and the option's detail explain why (and that existing panels' inverter needs to be one too). With switch-off unticked, they say what a curtailable inverter would avoid. On Amber, a battery should be one SmartShift can run (or one you automate). - New "Check the numbers" card for the option on show: average price per kWh bought and earned per kWh sent out, daily and demand charges and the bill, for the yardstick, the same plan with nothing new and the option; the plan's average buy and feed-in prices through the day and how often feed-in is below zero; rules of thumb (kWh a day per kW of panels, what each kWh of new solar or battery output is worth against what it costs per kWh, hours of negative feed-in, the planner's bill for the readings' plan); and the settings it was worked out with. - Help, README and tests updated. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L7aZZe8i8qyWD6cwhnihsr --- README.md | 2 +- css/app.css | 2 +- index.html | 54 ++++++----- js/constants.js | 2 +- js/help.js | 3 +- js/planner-ui.js | 218 +++++++++++++++++++++++++++++++++++++++++- tests/planner.spec.js | 44 +++++++++ tests/unit-time.js | 2 +- 8 files changed, 293 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 884afac..ca1cf7a 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ You need an Amber API key from [app.amber.com.au/developers](https://app.amber.c - Multi-site picker when your Amber account has more than one NMI - Editable Amber daily connection, subscription and demand rates for this financial year and last (days before 1 July 2026 use last year's); an uploaded bill fills the year it belongs to - More Stats: average import and feed-in prices, price-spike and negative-price exposure, evening import share, grid-free days -- Solar & battery planner: from the last 12 months of meter readings (Amber's, an imported Amber CSV, or any household's NEM12), tries every mix of new solar size, battery size and plan for the network, half hour by half hour, and ranks them by 20-year value, payback or yearly bill. Every option is measured against one yardstick: nothing new on the plan to compare against, by default whichever of the plans you'd use is cheapest with nothing new (switching is free). Companies, or single plans, can be ticked or unticked: only ticked plans are searched and shown. Any of the three can be fixed ("a size I choose", "none", or one plan). Shows next year's bill month by month against today's, a payback chart at today's prices and ±20% (payback is when it's ahead for good, after any battery replacement), worth in today's money and IRR (a battery replaced within the years compared is credited with the life it has left at the end), a "How keen to be" rating (Not worth it, Marginal, Modest, Good or Strong, from the value per $1 up front, whether it still pays with prices 20% lower, and payback, plus whether waiting a year would cost anything), and a 'Where the saving comes from' breakdown for each option (power bought, power sent to the grid, export and demand charges, and where the new panels' power goes: used at home, stored or sent out, lost to the export limit or clipped by the inverter), a "wait a year" check, and what just switching plan would save. Each plan is priced exactly as on Compare (free windows and caps, capped and time-varying feed-in, demand, Amber's export charge); missing Amber feed-in prices are rebuilt from AEMO wholesale prices. The battery charges from spare solar and from free or cheap grid hours, covers the home when power is dear and sells into high feed-in prices, knowing each day's prices in the morning. The export limit applies to the whole home and is measured against what the meter already sent out; a battery stores spare solar before the limit throws the rest away. When a feed-in price is negative the panels switch off by default (a battery soaks up what it can first), so nothing is paid to export; a setting says if they can't, and then exporting at negative prices costs money. The "How keen to be" rating also checks the result if feed-in prices from 10am to 3pm halve. Solar comes from a PVGIS 5.3 typical year (ERA5) for 22 towns (`data/solar`, fetched by `scripts/update-solar.js`), with sun position, tilt, facing, shade, temperature and inverter clipping, scaled to PVGIS's own yield. Homes that already have panels or a battery: the app spots them in the readings (solar sent out; selling after dark or quiet nights) and pre-fills a guess. Existing panels stay in the readings and new ones are added on top. An existing battery is taken out of the readings (selling to the grid is seen directly; what it covered at home is estimated from its size, the days it filled up and ran flat, and the home's use on nights it was empty; its charging from spare solar first, then from the grid in the cheapest half hours that bought more than usual) and run again on every plan with any new battery, since it would behave differently on another plan. A check card compares the planner's run of that battery on today's plan with the real readings, and shows what the battery saves. Allows for the federal solar (STC) and battery rebates shrinking each year, hardware getting cheaper, panel and battery wear, battery and inverter replacement, the export limit, and planned changes: an electric car, heat-pump hot water, gas heating and cooking to electric (with the gas bill saved) +- Solar & battery planner: from the last 12 months of meter readings (Amber's, an imported Amber CSV, or any household's NEM12), tries every mix of new solar size, battery size and plan for the network, half hour by half hour, and ranks them by 20-year value, payback or yearly bill. Every option is measured against one yardstick: nothing new on the plan to compare against, by default whichever of the plans you'd use is cheapest with nothing new (switching is free). Companies, or single plans, can be ticked or unticked: only ticked plans are searched and shown. Any of the three can be fixed ("a size I choose", "none", or one plan). Shows next year's bill month by month against today's, a payback chart at today's prices and ±20% (payback is when it's ahead for good, after any battery replacement), worth in today's money and IRR (a battery replaced within the years compared is credited with the life it has left at the end), a "How keen to be" rating (Not worth it, Marginal, Modest, Good or Strong, from the value per $1 up front, whether it still pays with prices 20% lower, and payback, plus whether waiting a year would cost anything), and a 'Where the saving comes from' breakdown for each option (power bought, power sent to the grid, export and demand charges, and where the new panels' power goes: used at home, stored or sent out, lost to the export limit or clipped by the inverter), a "wait a year" check, and what just switching plan would save. Each plan is priced exactly as on Compare (free windows and caps, capped and time-varying feed-in, demand, Amber's export charge); missing Amber feed-in prices are rebuilt from AEMO wholesale prices. The battery charges from spare solar and from free or cheap grid hours, covers the home when power is dear and sells into high feed-in prices, knowing each day's prices in the morning. The export limit applies to the whole home and is measured against what the meter already sent out; a battery stores spare solar before the limit throws the rest away. When a feed-in price is negative the panels switch off by default (a battery soaks up what it can first), so nothing is paid to export; a setting says if they can't, and then exporting at negative prices costs money. Where the feed-in price goes below zero (Amber), the recommendation and "what to ask installers for" say the panels need a "curtailable" inverter (one the retailer can switch off remotely), and on Amber a battery that SmartShift can run. A "Check the numbers" card for the option on show gives the average price paid per kWh bought and earned per kWh sent out (for the yardstick, the same plan with nothing new, and the option), daily and demand charges, the plan's average buy and feed-in prices through the day and how often feed-in is below zero, rules of thumb (kWh a day per kW of panels; what each kWh of new solar or battery output is worth against what it costs per kWh), and the settings it was worked out with. The "How keen to be" rating also checks the result if feed-in prices from 10am to 3pm halve. Solar comes from a PVGIS 5.3 typical year (ERA5) for 22 towns (`data/solar`, fetched by `scripts/update-solar.js`), with sun position, tilt, facing, shade, temperature and inverter clipping, scaled to PVGIS's own yield. Homes that already have panels or a battery: the app spots them in the readings (solar sent out; selling after dark or quiet nights) and pre-fills a guess. Existing panels stay in the readings and new ones are added on top. An existing battery is taken out of the readings (selling to the grid is seen directly; what it covered at home is estimated from its size, the days it filled up and ran flat, and the home's use on nights it was empty; its charging from spare solar first, then from the grid in the cheapest half hours that bought more than usual) and run again on every plan with any new battery, since it would behave differently on another plan. A check card compares the planner's run of that battery on today's plan with the real readings, and shows what the battery saves. Allows for the federal solar (STC) and battery rebates shrinking each year, hardware getting cheaper, panel and battery wear, battery and inverter replacement, the export limit, and planned changes: an electric car, heat-pump hot water, gas heating and cooking to electric (with the gas bill saved) ## Data completeness and CSV backups diff --git a/css/app.css b/css/app.css index 11894ea..b06b6f0 100644 --- a/css/app.css +++ b/css/app.css @@ -1 +1 @@ -*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}body{font-family:Segoe UI,system-ui,-apple-system,sans-serif}button:disabled{opacity:.5;cursor:not-allowed}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.plan-input{margin-top:.25rem;width:100%;border-radius:.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1));padding:.5rem .75rem;font-size:.875rem;line-height:1.25rem;--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity,1));--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.plan-input:focus{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity,1));outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity,1))}.plan-multi>summary{list-style:none}.plan-multi>summary::-webkit-details-marker{display:none}.main-tab{flex-shrink:0;white-space:nowrap;padding:.5rem .375rem;border-radius:.375rem;font-size:.8125rem;font-weight:600;color:#4b5563;transition:background-color .15s ease,color .15s ease}@media (min-width:640px){.main-tab{padding:.5rem 1rem;font-size:.875rem}}.main-tab:hover{background-color:#f3f4f6;color:#111827}.main-tab[aria-selected=true]{background-color:#4f46e5;color:#fff}.main-tab:focus-visible{outline:2px solid #6366f1;outline-offset:2px}.jump-link{font-size:.75rem;padding:.25rem .75rem;border-radius:9999px;border:1px solid #d1d5db;background:#fff;color:#4338ca}.jump-link:hover{background:#eef2ff}.calendar-day{min-height:85px;transition:background-color .2s}.calendar-day.has-data{cursor:pointer}#day-tooltip{pointer-events:none;transition:opacity .2s}.day-checkbox-label{display:flex;align-items:center;cursor:pointer;padding:2px 4px;border-radius:4px;transition:background-color .2s}.day-checkbox-label input:checked+span{font-weight:700;color:#1a56db}.day-checkbox-label:hover{background-color:#f0f0f0}.gst-switch{position:relative;display:inline-flex;width:2.75rem;height:1.5rem;flex-shrink:0;cursor:pointer}.gst-switch input{position:absolute;inset:0;z-index:1;margin:0;opacity:0;cursor:pointer}.gst-switch-track{display:block;width:100%;height:100%;border-radius:9999px;background:#e5e7eb;transition:background-color .15s ease}.gst-switch-track:after{content:"";position:absolute;top:2px;left:2px;width:1.25rem;height:1.25rem;border-radius:9999px;background:#fff;border:1px solid #d1d5db;transition:transform .15s ease}.gst-switch input:checked+.gst-switch-track{background:#2563eb}.gst-switch input:checked+.gst-switch-track:after{transform:translateX(1.25rem);border-color:#fff}.gst-switch input:focus-visible+.gst-switch-track{box-shadow:0 0 0 4px rgba(147,197,253,.8)}.help-btn{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:1.125rem;height:1.125rem;margin-left:.3rem;border-radius:9999px;border:1px solid #a5b4fc;background:#eef2ff;color:#4338ca;font-size:.7rem;font-weight:700;line-height:1;vertical-align:middle;cursor:help;text-transform:none;letter-spacing:0}.help-btn:hover,.help-btn[aria-expanded=true]{background:#4f46e5;border-color:#4f46e5;color:#fff}.help-btn:focus-visible{outline:2px solid #6366f1;outline-offset:2px}@media (pointer:coarse){.help-btn{position:relative}.help-btn:after{content:"";position:absolute;inset:-.6rem}}.help-row{display:flex;align-items:baseline;flex-wrap:nowrap}.help-row>h1,.help-row>h2,.help-row>h3,.help-row>h4,.help-row>label{min-width:0}.help-bubble{position:absolute;z-index:60;padding:.65rem .8rem;border-radius:.5rem;background:#1f2937;color:#f9fafb;font-size:.8125rem;font-weight:400;line-height:1.45;text-align:left;box-shadow:0 10px 25px rgba(0,0,0,.18)}.choice-card{display:flex;flex-direction:column;gap:.5rem;padding:1.25rem;border-radius:.75rem;border:2px solid #e5e7eb;background:#fff;text-align:left;transition:border-color .15s ease,box-shadow .15s ease}.choice-card:focus-visible,.choice-card:hover{border-color:#6366f1;box-shadow:0 4px 14px rgba(79,70,229,.15);outline:none}.step-num{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:1.5rem;height:1.5rem;border-radius:9999px;background:#eef2ff;color:#4338ca;font-size:.75rem;font-weight:700}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.left-0{left:0}.right-0{right:0}.right-1{right:.25rem}.top-1{top:.25rem}.top-10{top:2.5rem}.z-10{z-index:10}.z-20{z-index:20}.float-right{float:right}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-auto{margin-top:auto}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.\!table{display:table!important}.table{display:table}.grid{display:grid}.hidden{display:none}.h-12{height:3rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-7{height:1.75rem}.h-\[38vh\]{height:38vh}.h-\[50vh\]{height:50vh}.max-h-80{max-height:20rem}.max-h-96{max-height:24rem}.max-h-\[80vh\]{max-height:80vh}.w-0{width:0}.w-11\/12{width:91.666667%}.w-12{width:3rem}.w-2\.5{width:.625rem}.w-4{width:1rem}.w-6{width:1.5rem}.w-7{width:1.75rem}.w-72{width:18rem}.w-auto{width:auto}.w-full{width:100%}.min-w-full{min-width:100%}.max-w-2xl{max-width:42rem}.max-w-4xl{max-width:56rem}.max-w-\[200px\]{max-width:200px}.max-w-full{max-width:100%}.max-w-xl{max-width:36rem}.max-w-xs{max-width:20rem}.shrink{flex-shrink:1}.grow{flex-grow:1}.cursor-pointer{cursor:pointer}.resize{resize:both}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.flex-nowrap{flex-wrap:nowrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-1{row-gap:.25rem}.gap-y-2{row-gap:.5rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(229 231 235/var(--tw-divide-opacity,1))}.overflow-auto{overflow:auto}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-dotted{border-style:dotted}.border-amber-200{--tw-border-opacity:1;border-color:rgb(253 230 138/var(--tw-border-opacity,1))}.border-blue-200{--tw-border-opacity:1;border-color:rgb(191 219 254/var(--tw-border-opacity,1))}.border-emerald-200{--tw-border-opacity:1;border-color:rgb(167 243 208/var(--tw-border-opacity,1))}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity,1))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity,1))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity,1))}.border-gray-500{--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity,1))}.border-green-200{--tw-border-opacity:1;border-color:rgb(187 247 208/var(--tw-border-opacity,1))}.border-indigo-500{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity,1))}.border-red-200{--tw-border-opacity:1;border-color:rgb(254 202 202/var(--tw-border-opacity,1))}.border-sky-200{--tw-border-opacity:1;border-color:rgb(186 230 253/var(--tw-border-opacity,1))}.border-yellow-200{--tw-border-opacity:1;border-color:rgb(254 240 138/var(--tw-border-opacity,1))}.bg-amber-50{--tw-bg-opacity:1;background-color:rgb(255 251 235/var(--tw-bg-opacity,1))}.bg-amber-500{--tw-bg-opacity:1;background-color:rgb(245 158 11/var(--tw-bg-opacity,1))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity,1))}.bg-emerald-50{--tw-bg-opacity:1;background-color:rgb(236 253 245/var(--tw-bg-opacity,1))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity,1))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity,1))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.bg-green-100{--tw-bg-opacity:1;background-color:rgb(220 252 231/var(--tw-bg-opacity,1))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity,1))}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity,1))}.bg-green-600{--tw-bg-opacity:1;background-color:rgb(22 163 74/var(--tw-bg-opacity,1))}.bg-indigo-50{--tw-bg-opacity:1;background-color:rgb(238 242 255/var(--tw-bg-opacity,1))}.bg-indigo-600{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity,1))}.bg-orange-500{--tw-bg-opacity:1;background-color:rgb(249 115 22/var(--tw-bg-opacity,1))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity,1))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity,1))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-sky-50{--tw-bg-opacity:1;background-color:rgb(240 249 255/var(--tw-bg-opacity,1))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgb(254 252 232/var(--tw-bg-opacity,1))}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pl-5{padding-left:1.25rem}.pl-6{padding-left:1.5rem}.pr-2{padding-right:.5rem}.pr-6{padding-right:1.5rem}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[10px\]{font-size:10px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.leading-tight{line-height:1.25}.tracking-wider{letter-spacing:.05em}.text-amber-700{--tw-text-opacity:1;color:rgb(180 83 9/var(--tw-text-opacity,1))}.text-amber-800{--tw-text-opacity:1;color:rgb(146 64 14/var(--tw-text-opacity,1))}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity,1))}.text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity,1))}.text-emerald-700{--tw-text-opacity:1;color:rgb(4 120 87/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity,1))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity,1))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity,1))}.text-green-700{--tw-text-opacity:1;color:rgb(21 128 61/var(--tw-text-opacity,1))}.text-green-800{--tw-text-opacity:1;color:rgb(22 101 52/var(--tw-text-opacity,1))}.text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity,1))}.text-orange-600{--tw-text-opacity:1;color:rgb(234 88 12/var(--tw-text-opacity,1))}.text-orange-700{--tw-text-opacity:1;color:rgb(194 65 12/var(--tw-text-opacity,1))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity,1))}.text-red-800{--tw-text-opacity:1;color:rgb(153 27 27/var(--tw-text-opacity,1))}.text-sky-700{--tw-text-opacity:1;color:rgb(3 105 161/var(--tw-text-opacity,1))}.text-sky-900{--tw-text-opacity:1;color:rgb(12 74 110/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-yellow-600{--tw-text-opacity:1;color:rgb(202 138 4/var(--tw-text-opacity,1))}.text-yellow-700{--tw-text-opacity:1;color:rgb(161 98 7/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-amber-400{--tw-ring-opacity:1;--tw-ring-color:rgb(251 191 36/var(--tw-ring-opacity,1))}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.hover\:bg-gray-300:hover{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity,1))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.hover\:bg-indigo-50:hover{--tw-bg-opacity:1;background-color:rgb(238 242 255/var(--tw-bg-opacity,1))}.hover\:bg-indigo-700:hover{--tw-bg-opacity:1;background-color:rgb(67 56 202/var(--tw-bg-opacity,1))}.hover\:text-blue-800:hover{--tw-text-opacity:1;color:rgb(30 64 175/var(--tw-text-opacity,1))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.hover\:text-red-500:hover{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.hover\:underline:hover{text-decoration-line:underline}.focus\:not-sr-only:focus{position:static;width:auto;height:auto;padding:0;margin:0;overflow:visible;clip:auto;white-space:normal}.focus\:absolute:focus{position:absolute}.focus\:left-2:focus{left:.5rem}.focus\:top-2:focus{top:.5rem}.focus\:z-50:focus{z-index:50}.focus\:rounded:focus{border-radius:.25rem}.focus\:border-indigo-500:focus{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity,1))}.focus\:bg-white:focus{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.focus\:px-3:focus{padding-left:.75rem;padding-right:.75rem}.focus\:py-2:focus{padding-top:.5rem;padding-bottom:.5rem}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity,1))}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width:640px){.sm\:left-auto{left:auto}.sm\:col-span-3{grid-column:span 3/span 3}.sm\:hidden{display:none}.sm\:h-14{height:3.5rem}.sm\:w-14{width:3.5rem}.sm\:w-80{width:20rem}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.sm\:flex-wrap{flex-wrap:wrap}.sm\:gap-1{gap:.25rem}.sm\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.sm\:p-6{padding:1.5rem}.sm\:text-2xl{font-size:1.5rem;line-height:2rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}}@media (min-width:768px){.md\:h-\[58vh\]{height:58vh}.md\:h-\[70vh\]{height:70vh}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:p-8{padding:2rem}} \ No newline at end of file +*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}body{font-family:Segoe UI,system-ui,-apple-system,sans-serif}button:disabled{opacity:.5;cursor:not-allowed}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.plan-input{margin-top:.25rem;width:100%;border-radius:.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1));padding:.5rem .75rem;font-size:.875rem;line-height:1.25rem;--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity,1));--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.plan-input:focus{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity,1));outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity,1))}.plan-multi>summary{list-style:none}.plan-multi>summary::-webkit-details-marker{display:none}.main-tab{flex-shrink:0;white-space:nowrap;padding:.5rem .375rem;border-radius:.375rem;font-size:.8125rem;font-weight:600;color:#4b5563;transition:background-color .15s ease,color .15s ease}@media (min-width:640px){.main-tab{padding:.5rem 1rem;font-size:.875rem}}.main-tab:hover{background-color:#f3f4f6;color:#111827}.main-tab[aria-selected=true]{background-color:#4f46e5;color:#fff}.main-tab:focus-visible{outline:2px solid #6366f1;outline-offset:2px}.jump-link{font-size:.75rem;padding:.25rem .75rem;border-radius:9999px;border:1px solid #d1d5db;background:#fff;color:#4338ca}.jump-link:hover{background:#eef2ff}.calendar-day{min-height:85px;transition:background-color .2s}.calendar-day.has-data{cursor:pointer}#day-tooltip{pointer-events:none;transition:opacity .2s}.day-checkbox-label{display:flex;align-items:center;cursor:pointer;padding:2px 4px;border-radius:4px;transition:background-color .2s}.day-checkbox-label input:checked+span{font-weight:700;color:#1a56db}.day-checkbox-label:hover{background-color:#f0f0f0}.gst-switch{position:relative;display:inline-flex;width:2.75rem;height:1.5rem;flex-shrink:0;cursor:pointer}.gst-switch input{position:absolute;inset:0;z-index:1;margin:0;opacity:0;cursor:pointer}.gst-switch-track{display:block;width:100%;height:100%;border-radius:9999px;background:#e5e7eb;transition:background-color .15s ease}.gst-switch-track:after{content:"";position:absolute;top:2px;left:2px;width:1.25rem;height:1.25rem;border-radius:9999px;background:#fff;border:1px solid #d1d5db;transition:transform .15s ease}.gst-switch input:checked+.gst-switch-track{background:#2563eb}.gst-switch input:checked+.gst-switch-track:after{transform:translateX(1.25rem);border-color:#fff}.gst-switch input:focus-visible+.gst-switch-track{box-shadow:0 0 0 4px rgba(147,197,253,.8)}.help-btn{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:1.125rem;height:1.125rem;margin-left:.3rem;border-radius:9999px;border:1px solid #a5b4fc;background:#eef2ff;color:#4338ca;font-size:.7rem;font-weight:700;line-height:1;vertical-align:middle;cursor:help;text-transform:none;letter-spacing:0}.help-btn:hover,.help-btn[aria-expanded=true]{background:#4f46e5;border-color:#4f46e5;color:#fff}.help-btn:focus-visible{outline:2px solid #6366f1;outline-offset:2px}@media (pointer:coarse){.help-btn{position:relative}.help-btn:after{content:"";position:absolute;inset:-.6rem}}.help-row{display:flex;align-items:baseline;flex-wrap:nowrap}.help-row>h1,.help-row>h2,.help-row>h3,.help-row>h4,.help-row>label{min-width:0}.help-bubble{position:absolute;z-index:60;padding:.65rem .8rem;border-radius:.5rem;background:#1f2937;color:#f9fafb;font-size:.8125rem;font-weight:400;line-height:1.45;text-align:left;box-shadow:0 10px 25px rgba(0,0,0,.18)}.choice-card{display:flex;flex-direction:column;gap:.5rem;padding:1.25rem;border-radius:.75rem;border:2px solid #e5e7eb;background:#fff;text-align:left;transition:border-color .15s ease,box-shadow .15s ease}.choice-card:focus-visible,.choice-card:hover{border-color:#6366f1;box-shadow:0 4px 14px rgba(79,70,229,.15);outline:none}.step-num{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:1.5rem;height:1.5rem;border-radius:9999px;background:#eef2ff;color:#4338ca;font-size:.75rem;font-weight:700}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.left-0{left:0}.right-0{right:0}.right-1{right:.25rem}.top-1{top:.25rem}.top-10{top:2.5rem}.z-10{z-index:10}.z-20{z-index:20}.float-right{float:right}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-auto{margin-top:auto}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.\!table{display:table!important}.table{display:table}.grid{display:grid}.hidden{display:none}.h-12{height:3rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-7{height:1.75rem}.h-\[38vh\]{height:38vh}.h-\[50vh\]{height:50vh}.max-h-80{max-height:20rem}.max-h-96{max-height:24rem}.max-h-\[80vh\]{max-height:80vh}.w-0{width:0}.w-11\/12{width:91.666667%}.w-12{width:3rem}.w-2\.5{width:.625rem}.w-4{width:1rem}.w-6{width:1.5rem}.w-7{width:1.75rem}.w-72{width:18rem}.w-auto{width:auto}.w-full{width:100%}.min-w-full{min-width:100%}.max-w-2xl{max-width:42rem}.max-w-4xl{max-width:56rem}.max-w-\[200px\]{max-width:200px}.max-w-full{max-width:100%}.max-w-xl{max-width:36rem}.max-w-xs{max-width:20rem}.shrink{flex-shrink:1}.grow{flex-grow:1}.cursor-pointer{cursor:pointer}.resize{resize:both}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.flex-nowrap{flex-wrap:nowrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-1{row-gap:.25rem}.gap-y-2{row-gap:.5rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(229 231 235/var(--tw-divide-opacity,1))}.overflow-auto{overflow:auto}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-dotted{border-style:dotted}.border-amber-200{--tw-border-opacity:1;border-color:rgb(253 230 138/var(--tw-border-opacity,1))}.border-blue-200{--tw-border-opacity:1;border-color:rgb(191 219 254/var(--tw-border-opacity,1))}.border-emerald-200{--tw-border-opacity:1;border-color:rgb(167 243 208/var(--tw-border-opacity,1))}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity,1))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity,1))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity,1))}.border-gray-500{--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity,1))}.border-green-200{--tw-border-opacity:1;border-color:rgb(187 247 208/var(--tw-border-opacity,1))}.border-indigo-500{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity,1))}.border-red-200{--tw-border-opacity:1;border-color:rgb(254 202 202/var(--tw-border-opacity,1))}.border-sky-200{--tw-border-opacity:1;border-color:rgb(186 230 253/var(--tw-border-opacity,1))}.border-yellow-200{--tw-border-opacity:1;border-color:rgb(254 240 138/var(--tw-border-opacity,1))}.bg-amber-50{--tw-bg-opacity:1;background-color:rgb(255 251 235/var(--tw-bg-opacity,1))}.bg-amber-500{--tw-bg-opacity:1;background-color:rgb(245 158 11/var(--tw-bg-opacity,1))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity,1))}.bg-emerald-50{--tw-bg-opacity:1;background-color:rgb(236 253 245/var(--tw-bg-opacity,1))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity,1))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity,1))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.bg-green-100{--tw-bg-opacity:1;background-color:rgb(220 252 231/var(--tw-bg-opacity,1))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity,1))}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity,1))}.bg-green-600{--tw-bg-opacity:1;background-color:rgb(22 163 74/var(--tw-bg-opacity,1))}.bg-indigo-50{--tw-bg-opacity:1;background-color:rgb(238 242 255/var(--tw-bg-opacity,1))}.bg-indigo-600{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity,1))}.bg-orange-500{--tw-bg-opacity:1;background-color:rgb(249 115 22/var(--tw-bg-opacity,1))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity,1))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity,1))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-sky-50{--tw-bg-opacity:1;background-color:rgb(240 249 255/var(--tw-bg-opacity,1))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgb(254 252 232/var(--tw-bg-opacity,1))}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pl-5{padding-left:1.25rem}.pl-6{padding-left:1.5rem}.pr-2{padding-right:.5rem}.pr-6{padding-right:1.5rem}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[10px\]{font-size:10px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.leading-tight{line-height:1.25}.tracking-wider{letter-spacing:.05em}.text-amber-700{--tw-text-opacity:1;color:rgb(180 83 9/var(--tw-text-opacity,1))}.text-amber-800{--tw-text-opacity:1;color:rgb(146 64 14/var(--tw-text-opacity,1))}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity,1))}.text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity,1))}.text-emerald-700{--tw-text-opacity:1;color:rgb(4 120 87/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity,1))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity,1))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity,1))}.text-green-700{--tw-text-opacity:1;color:rgb(21 128 61/var(--tw-text-opacity,1))}.text-green-800{--tw-text-opacity:1;color:rgb(22 101 52/var(--tw-text-opacity,1))}.text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity,1))}.text-orange-600{--tw-text-opacity:1;color:rgb(234 88 12/var(--tw-text-opacity,1))}.text-orange-700{--tw-text-opacity:1;color:rgb(194 65 12/var(--tw-text-opacity,1))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity,1))}.text-red-800{--tw-text-opacity:1;color:rgb(153 27 27/var(--tw-text-opacity,1))}.text-sky-700{--tw-text-opacity:1;color:rgb(3 105 161/var(--tw-text-opacity,1))}.text-sky-900{--tw-text-opacity:1;color:rgb(12 74 110/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-yellow-600{--tw-text-opacity:1;color:rgb(202 138 4/var(--tw-text-opacity,1))}.text-yellow-700{--tw-text-opacity:1;color:rgb(161 98 7/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-amber-400{--tw-ring-opacity:1;--tw-ring-color:rgb(251 191 36/var(--tw-ring-opacity,1))}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.hover\:bg-gray-300:hover{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity,1))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.hover\:bg-indigo-50:hover{--tw-bg-opacity:1;background-color:rgb(238 242 255/var(--tw-bg-opacity,1))}.hover\:bg-indigo-700:hover{--tw-bg-opacity:1;background-color:rgb(67 56 202/var(--tw-bg-opacity,1))}.hover\:text-blue-800:hover{--tw-text-opacity:1;color:rgb(30 64 175/var(--tw-text-opacity,1))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.hover\:text-red-500:hover{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.hover\:underline:hover{text-decoration-line:underline}.focus\:not-sr-only:focus{position:static;width:auto;height:auto;padding:0;margin:0;overflow:visible;clip:auto;white-space:normal}.focus\:absolute:focus{position:absolute}.focus\:left-2:focus{left:.5rem}.focus\:top-2:focus{top:.5rem}.focus\:z-50:focus{z-index:50}.focus\:rounded:focus{border-radius:.25rem}.focus\:border-indigo-500:focus{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity,1))}.focus\:bg-white:focus{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.focus\:px-3:focus{padding-left:.75rem;padding-right:.75rem}.focus\:py-2:focus{padding-top:.5rem;padding-bottom:.5rem}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity,1))}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width:640px){.sm\:left-auto{left:auto}.sm\:col-span-3{grid-column:span 3/span 3}.sm\:mb-0{margin-bottom:0}.sm\:hidden{display:none}.sm\:h-14{height:3.5rem}.sm\:w-14{width:3.5rem}.sm\:w-80{width:20rem}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.sm\:flex-wrap{flex-wrap:wrap}.sm\:gap-1{gap:.25rem}.sm\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.sm\:p-6{padding:1.5rem}.sm\:text-2xl{font-size:1.5rem;line-height:2rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}}@media (min-width:768px){.md\:h-\[58vh\]{height:58vh}.md\:h-\[70vh\]{height:70vh}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:p-8{padding:2rem}} \ No newline at end of file diff --git a/index.html b/index.html index 0bc0819..e6220b2 100644 --- a/index.html +++ b/index.html @@ -4,9 +4,9 @@ - Amber Compare & Export v1.58 + Amber Compare & Export v1.59 - + Skip to the tabs @@ -19,7 +19,7 @@

Amber Compare & Export

See what your electricity really costs on Amber, check an Amber bill, and find out whether another plan (or Amber) would be cheaper for you.

-

Free and private: everything runs in this browser and nothing is uploaded. Version 1.58.

+

Free and private: everything runs in this browser and nothing is uploaded. Version 1.59.

- +
@@ -764,6 +764,10 @@

Your options

Next year, month by month

+
+

Check the numbers

+
+

Paying it back

@@ -794,26 +798,26 @@

Paying - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/js/constants.js b/js/constants.js index 31cd4ae..89716f4 100644 --- a/js/constants.js +++ b/js/constants.js @@ -12,7 +12,7 @@ // Compare "This month" when the page opens with a saved API key (a test can preset // window.Amber.AUTO_COMPARE = false to start idle). Amber.AUTO_COMPARE = Amber.AUTO_COMPARE !== false; - Amber.APP_VERSION = '1.58'; + Amber.APP_VERSION = '1.59'; Amber.TEMPLATES_AS_AT = '2026-07-01'; // All editable rates (plans, custom, Amber fixed charges) are GST-inclusive, // matching Energy Made Easy, DMO/VDO, retailer fact sheets and Amber perKwh. diff --git a/js/help.js b/js/help.js index eb45cae..9dde1ad 100644 --- a/js/help.js +++ b/js/help.js @@ -122,7 +122,8 @@ planOptions: 'Each row is one mix of new solar, battery and plan, with next year\'s bill, what it saves, when it pays back and what it\'s worth over the years. Tap the ? on a column heading for what it counts. Tap a row to see its year month by month and its payback chart.', planEnthusiasm: 'A quick verdict on the option. Strong: every $1 you pay up front comes back as $1.75 or more in today\'s money, it still pays if power prices are 20% lower than you set (and, with new panels, if feed-in prices from 10am to 3pm halve), and it pays back within 8 years. Good: $1.40 or more back, and still pays in both of those cases. Modest: $1.15 or more back. Marginal: just over $1 back. Not worth it: less back than you put in. It also says whether waiting a year would cost you anything. It\'s a rule of thumb: how keen to be also depends on how sure you are about prices, rebates and your plans for the home.', planBreakdown: 'The option against the same plan with nothing new, part by part: what you\'d pay for power bought, what you\'d earn for power sent to the grid, and any export or demand charge. Underneath, where the new panels\' power goes over a year: used at home, stored or sent out, or lost to the export limit or the inverter. On Amber, power sent out earns last year\'s actual feed-in prices for each half hour; if a lot more solar is installed around you, daytime prices could be lower in future.', - planSwitchOff: 'On Amber the feed-in price goes negative at times, mostly in the middle of sunny days: sending power out then costs you. Ticked: your panels stop sending power out at those times (Amber can switch off inverters it supports; a battery soaks up what it can first), so you neither pay nor earn. Untick if your inverter can\'t: the planner then charges you for power sent out at negative prices.', + planSwitchOff: 'On Amber the feed-in price goes negative at times, mostly in the middle of sunny days: sending power out then costs you. Ticked: your panels stop sending power out at those times (a battery soaks up what it can first), so you neither pay nor earn. That takes a "curtailable" inverter: one Amber can switch off remotely, from Amber\'s list of supported inverters, so ask for one if you\'re buying and check the one you have. Untick if your inverter can\'t: the planner then charges you for power sent out at negative prices.', + planNumbers: 'The averages behind the option on show (tap a row in "Your options" to change it), so you can check they look right. Compare the price per kWh bought and the daily charges with your bills or the plan\'s price list, and the panels\' kWh a day per kW with installers\' quotes. As a rule of thumb, each kWh from new panels should be worth somewhere between the feed-in price and the price of power (more of it used at home means nearer the price of power), and should be worth more than it costs; a battery should save more per kWh it gives back than it costs per kWh. Prices through the day are plain averages of every half hour, to show when power is cheap or dear.', planPayback: 'The total you\'re ahead (or behind) each year after paying for the system, in plain dollars, against nothing new on the plan you compare against. Where the line crosses zero for good is the payback year. Replacing the battery and inverter makes it dip, and can put you behind again for a while. The dashed lines show power prices 20% higher or lower.', // ---------- Glossary ---------- diff --git a/js/planner-ui.js b/js/planner-ui.js index 13dc44d..3e360f5 100644 --- a/js/planner-ui.js +++ b/js/planner-ui.js @@ -469,6 +469,11 @@ : [0].concat(BATTERY_SIZES.filter((b) => b < maxBattery), [maxBattery]); const amberModel = Amber.amberPlannerModel(searchYear, site, app().readAmberRates()); + const town = Amber.SOLAR_LOCATIONS.find((l) => l.id === $('planLocation').value); + const setup = { + town: town ? town.name : 'your area', facing: (Amber.SOLAR_ORIENTATIONS[$('planOrientation').value] || { label: 'North' }).label, + tilt: num('planTilt'), shading: num('planShading'), exportLimit: num('planExportLimit') + }; const t0 = Date.now(); const res = await Amber.plannerSearch({ year: searchYear, plans, state, amberModel, solarPerKw, solarSizes, batterySizes, @@ -483,7 +488,7 @@ const at = p.phase === 'existing' ? 0.04 + 0.11 * p.done / p.total : p.phase === 'probe' ? 0.15 + 0.1 * p.done / p.total : 0.25 + 0.75 * p.done / p.total; progress(text, at); }); - ui.results = analyse(res, { f, compareWith, allow: new Set(allow), solarMode, batteryMode, solarSizes, fixedBattery, loads, weather, plans, existing, unwound, switchOff: $('planSwitchOff').checked, seconds: (Date.now() - t0) / 1000 }); + ui.results = analyse(res, { f, compareWith, allow: new Set(allow), solarMode, batteryMode, solarSizes, fixedBattery, loads, weather, plans, existing, unwound, setup, switchOff: $('planSwitchOff').checked, seconds: (Date.now() - t0) / 1000 }); ui.results.readingsPlan = readingsPlan || ui.results.free.plan; ui.results.check = unwound ? Amber.plannerExistingCheck(res.prep, ui.results.readingsPlan, year) : null; progress(`Done: ${ui.results.options.length.toLocaleString('en-AU')} options compared.`, null); @@ -542,10 +547,64 @@ return list; } + // How many half hours of the year this plan's feed-in price is below zero (Amber's wholesale + // prices), and how many of them are from 10am to 3pm. Cached per plan. + function negativeFeedIn(planName) { + const r = ui.results; + r.negCache = r.negCache || {}; + if (!r.negCache[planName]) { + const prep = r.res.prep; + const sell = prep.pricesOf(prep.byName[planName]).sell; + let all = 0; + let midday = 0; + for (let i = 0; i < sell.length; i++) { + if (!(sell[i] < 0)) continue; + all += 1; + const s = i % 48; + if (s >= 20 && s < 30) midday += 1; + } + r.negCache[planName] = { all, midday }; + } + return r.negCache[planName]; + } + + // Panels on the roof already: entered, or solar sent out in the readings. + function solarNow() { + const r = ui.results; + if (r.solarNow == null) { + const y = r.res.prep.year; + const m = y.meterExp || y.exp; + let sent = 0; + for (let i = 0; i < m.length; i++) sent += m[i]; + r.solarNow = r.existing.solarKw > 0 || sent > 10; + } + return r.solarNow; + } + + // Switching the panels off when the feed-in price is below zero takes an inverter the retailer + // can switch off remotely: a "curtailable" one. + const needsCurtailable = (x) => ui.results.switchOff && negativeFeedIn(x.plan).all > 0; + + function curtailNote(x) { + const r = ui.results; + const neg = negativeFeedIn(x.plan); + if (!neg.all || !(x.solarKw > 0 || solarNow())) return null; + const amber = r.res.prep.byName[x.plan].amber; + const who = amber ? 'Amber' : 'the retailer'; + const when = `${amber ? 'On Amber' : 'On this plan'} the feed-in price goes below zero at times (${Math.round(neg.all / 2).toLocaleString('en-AU')} hours over your readings' year, mostly in the middle of sunny days)`; + if (!r.switchOff) { + return { lead: 'A "curtailable" inverter would help.', text: `${when}. You've said your panels can't switch off, so the planner counts what sending power out then costs you. A "curtailable" inverter, one ${who} can switch off remotely, would avoid that.` }; + } + const yours = x.solarKw > 0 + ? `Ask for a "curtailable" inverter: one ${who} can switch off remotely (check it's on ${who}'s list of supported inverters before you buy).${solarNow() ? ' The inverter for your current panels needs to be one too.' : ''}` + : `That needs your inverter to be "curtailable": one ${who} can switch off remotely (check ${who}'s list of supported inverters).`; + return { lead: 'Curtailable inverter needed.', text: `${when}, and the planner assumes your panels stop sending power out then. ${yours} If that's not possible, untick "Panels switch off when the feed-in price is negative" and run again.` }; + } + // What to ask installers for: the sizes the planner assumed for this option (new equipment only). function orderText(x) { const parts = []; - if (x.solarKw > 0) parts.push(`${kw(x.solarKw)} of panels with an inverter of about ${kw(x.solarKw / INVERTER_RATIO)}`); + if (x.solarKw > 0) parts.push(`${kw(x.solarKw)} of panels with ${needsCurtailable(x) ? 'a "curtailable" inverter' : 'an inverter'} of about ${kw(x.solarKw / INVERTER_RATIO)}`); if (x.batteryKwh > 0) { const b = ui.results.res.prep.ctx.battery || {}; const power = Math.min(15, Math.max(2.5, x.batteryKwh * (b.kwPerKwh || 0.4))); @@ -558,6 +617,7 @@ const notes = ['These are the sizes the planner assumed.']; if (x.solarKw > 0) notes.push('A bigger inverter loses a little less on sunny middays.'); if (x.solarKw > 0 && x.batteryKwh > 0) notes.push('The battery\'s power is treated as separate from the panels\' inverter: if one hybrid inverter will run both, ask for one that can handle both.'); + if (x.batteryKwh > 0 && ui.results.res.prep.byName[x.plan].amber) notes.push('On Amber, choose a battery Amber\'s SmartShift can run (or one you can automate yourself), so it follows the prices the way the planner assumes.'); return notes.join(' '); } @@ -683,6 +743,8 @@ lines.push(`

What to ask installers for: ${esc(orderText(best))}. ${esc(orderNote(best))}

`); lines.push(`

Up front: ${dollars(fin.capex.total)} after ${dollars(fin.capex.rebates)} of rebates. It ${paybackText(fin)}, and over ${r.f.years} years it's worth ${dollars(fin.npv)} in today's money${fin.irr != null ? ` (like ${(fin.irr * 100).toFixed(1)} % interest a year)` : ''}.

`); } + const curtail = curtailNote(best); + if (curtail) lines.push(`

${esc(curtail.lead)} ${esc(curtail.text)}

`); if (r.free.plan !== r.compare.plan && r.base - r.free.bill > 1) { const onTop = `Payback and worth here include that, because you're comparing against ${esc(label(r.compare.plan))}: pick "Whatever's cheapest" to count only what the panels and battery add.`; lines.push(`

${hardware ? 'Without buying anything, just' : 'Just'} switching to ${esc(label(r.free.plan))} would save ${dollars(r.base - r.free.bill)} a year. ${hardware ? onTop : ''}

`); @@ -874,6 +936,8 @@ const nowImport = now.imp.reduce((s, v) => s + v, 0) + now.cl.reduce((s, v) => s + v, 0); const change = nowImport > 0 ? Math.round((tot.imp / nowImport - 1) * 100) : 0; if (x.solarKw > 0 || x.batteryKwh > 0) facts.push(`What to ask installers for: ${orderText(x)}. ${orderNote(x)}`); + const curtail = curtailNote(x); + if (curtail) facts.push(`${curtail.lead} ${curtail.text}`); facts.push(`Power bought from the grid: ${Math.round(nowImport).toLocaleString('en-AU')} → ${Math.round(tot.imp).toLocaleString('en-AU')} kWh a year${change ? ` (${Math.abs(change)} % ${change < 0 ? 'less' : 'more'})` : ''}.${change > 0 && ev.sim ? ' The battery buys extra when power is cheap, to use or sell when it\'s dear, and loses a little on the way.' : ''}`); if (x.solarKw > 0) { const f = ev.flows; @@ -885,11 +949,157 @@ facts.push(`${which} cycle${r.existing.kwh > 0 && x.batteryKwh > 0 ? '' : 's'} about ${Math.round(ev.sim.cycles)} times a year (${(ev.sim.cycles / 365).toFixed(1)} a day)${ev.sim.gridCharged > 1 ? `, charging ${Math.round(ev.sim.gridCharged).toLocaleString('en-AU')} kWh from the grid when it's cheap or free` : ''}${ev.sim.exported > 1 ? ` and selling ${Math.round(ev.sim.exported).toLocaleString('en-AU')} kWh back when prices are high` : ''}.`); } html += `
    ${facts.map((t) => `
  • ${esc(t)}
  • `).join('')}
`; - html += breakdownHtml(x, ev, Amber.plannerEvaluate(prep, x.plan, 0, 0)); + const base = Amber.plannerEvaluate(prep, x.plan, 0, 0); + html += breakdownHtml(x, ev, base); $('planDetailBody').innerHTML = html; + renderNumbers(x, ev, base, now); renderPayback(x); } + // ---------- Check the numbers ---------- + // Average prices, the prices through the day, per-kWh rules of thumb and the settings used for + // the option on show, to check against bills, price lists and installers' quotes. + const BANDS = [ + { name: 'Night (9pm to 7am)', from: 42, to: 14 }, + { name: 'Morning (7am to 10am)', from: 14, to: 20 }, + { name: 'Middle of the day (10am to 3pm)', from: 20, to: 30 }, + { name: 'Afternoon and evening (3pm to 9pm)', from: 30, to: 42 }, + { name: 'All day', from: 0, to: 48 } + ]; + const cents = (v) => `${v < -0.05 ? '−' : ''}${Math.abs(v).toFixed(1)} c`; + + // A year's averages: c/kWh paid for power bought (energy only), c/kWh earned for power sent out + // (after any network export charge), daily charges per day. + function averagesOf(ev, days) { + const p = billParts(ev); + const earned = -p.sold - p.exportCharge; + return { + boughtKwh: p.boughtKwh, soldKwh: p.soldKwh, + buy: p.boughtKwh > 0.5 ? (p.bought / p.boughtKwh) * 100 : null, + sell: p.soldKwh > 0.5 ? (earned / p.soldKwh) * 100 : null, + perDay: p.fixed / days, demand: p.demand, total: p.total + }; + } + + // The plain average of each time of day's half-hourly prices over the year (c/kWh). + function bandPrices(planName) { + const prep = ui.results.res.prep; + const pr = prep.pricesOf(prep.byName[planName]); + return BANDS.map((b) => { + let buy = 0; + let sell = 0; + let n = 0; + let neg = 0; + for (let i = 0; i < pr.buy.length; i++) { + const s = i % 48; + if (b.from < b.to ? (s < b.from || s >= b.to) : (s < b.from && s >= b.to)) continue; + buy += pr.buy[i]; + sell += pr.sell[i]; + n += 1; + if (pr.sell[i] < 0) neg += 1; + } + return { name: b.name, buy: buy / n, sell: sell / n, neg: neg / n }; + }); + } + + function capexText(x) { + const f = ui.results.f; + const parts = []; + if (x.solarKw > 0) parts.push(`panels ${dollars(f.solarFixed + f.solarPerKw * x.solarKw)} (${dollars(f.solarPerKw)} a kW plus ${dollars(f.solarFixed)}) less a ${dollars(f.stcPerKw * x.solarKw)} rebate`); + if (x.batteryKwh > 0) parts.push(`battery ${dollars(f.batteryFixed + f.batteryPerKwh * x.batteryKwh)} (${dollars(f.batteryPerKwh)} a kWh plus ${dollars(f.batteryFixed)}) less a ${dollars(f.batteryRebatePerKwh * x.batteryKwh)} rebate`); + return `${parts.join('; ')}: ${dollars(x.fin.capex.total)} in all`; + } + + function renderNumbers(x, ev, base, now) { + const r = ui.results; + const prep = r.res.prep; + const f = r.f; + const s = r.setup; + const days = prep.year.days; + const k = (v) => Math.round(v).toLocaleString('en-AU'); + const plan = prep.byName[x.plan]; + const hardware = x.solarKw > 0 || x.batteryKwh > 0; + + // The yardstick, this plan with nothing new, and the option. + const cols = [{ head: `Nothing new on ${shortLabel(r.compare.plan)}`, a: averagesOf(now, days) }]; + if (x.plan !== r.compare.plan) cols.push({ head: `Nothing new on ${shortLabel(x.plan)}`, a: averagesOf(base, days) }); + if (hardware) cols.push({ head: `${system(x)} on ${shortLabel(x.plan)}`, a: averagesOf(ev, days) }); + const cell = (t) => `${t}`; + const row = (name, fn) => `${name}${cols.map((c) => cell(fn(c.a))).join('')}`; + let html = `

The averages behind ${esc(describe(x))} on ${esc(label(x.plan))}, from the planner's half-hour-by-half-hour year, to check against your bills, the plan's price list and installers' quotes. Prices include GST.

` + + `${th('A year')}${cols.map((c) => th(esc(c.head), 1)).join('')}` + + row('Power bought (kWh)', (a) => k(a.boughtKwh)) + + row('Average price per kWh bought', (a) => (a.buy == null ? '—' : cents(a.buy))) + + row('Power sent out (kWh)', (a) => k(a.soldKwh)) + + row('Average earned per kWh sent out', (a) => (a.sell == null ? '—' : cents(a.sell))) + + row('Daily charges', (a) => `$${a.perDay.toFixed(2)} a day`) + + (cols.some((c) => c.a.demand > 0.5) ? row('Demand charges', (a) => dollars(a.demand)) : '') + + `${cols.map((c) => cell(dollars(c.a.total))).join('')}
Bill
` + + '

Average price per kWh bought is the energy part of the bill over the kWh bought (daily and demand charges are separate). Average earned per kWh sent out is the feed-in credit, less any network export charge, over the kWh sent out.

'; + + // The plan's prices through the day. + const bands = bandPrices(x.plan); + const anyNeg = bands.some((b) => b.neg > 0); + html += `

${esc(label(x.plan))}: prices through the day

` + + `

The plain average of every half hour in the year, per kWh${plan.amber ? ', from Amber\'s prices over your readings\' year' : `, at the plan's rates as at ${fmtDate(Amber.TEMPLATES_AS_AT)}`}. The averages above are weighted by when you actually buy and send out power. Times are the electricity market's clock (Brisbane time), so in daylight saving they're about an hour behind the clock on the wall.

` + + `${th('Time of day')}${th('Buy (c/kWh)', 1)}${th('Feed-in (c/kWh)', 1)}${anyNeg ? th('Feed-in below zero', 1) : ''}` + + bands.map((b, i) => `${cell(cents(b.buy))}${cell(cents(b.sell))}${anyNeg ? cell(`${Math.round(b.neg * 100)} %`) : ''}`).join('') + + '
${b.name}
'; + + // Rules of thumb: what the new equipment makes and is worth per kWh, against what it costs. + const notes = []; + if (x.solarKw > 0) { + const clipped = (prep.ctx.solarPerKw && prep.ctx.solarPerKw.clipped || 0) * x.solarKw; + const made = ev.flows.solarKwh + clipped; + notes.push(`Panels: the new ${kw(x.solarKw)} make about ${(made / x.solarKw / days).toFixed(1)} kWh a day for each kW of panels (${k(made / x.solarKw)} a year), facing ${s.facing.toLowerCase()} at ${s.tilt}° with ${s.shading} % shade, in ${s.town}'s typical weather. For panels facing north, installers often quote about 3.5 (Hobart, Melbourne) to 4.4 (Perth, Darwin).`); + if (x.opt && made > 0) { + const worth = x.opt.planOnly - x.opt.solarOnly; + let lifeKwh = 0; + for (let y = 0; y < f.years; y++) lifeKwh += made * Math.pow(1 - f.solarDegradation / 100, y); + notes.push(`Each kWh the new panels make is worth about ${cents((worth / made) * 100)} to you in the first year: what's used at home saves the price of power, what's sent out earns the feed-in price, and some is lost (the export limit, switch-offs, the inverter). They cost ${dollars(x.fin.capex.solar)} after the rebate, about ${cents((x.fin.capex.solar / lifeKwh) * 100)} for each kWh they make over ${f.years} years (before interest and price rises). Worth more per kWh than they cost is a good sign.`); + } + } + if (x.batteryKwh > 0 && ev.sim && x.opt) { + const without = Amber.plannerEvaluate(prep, x.plan, x.solarKw, 0); + const delivered = ev.sim.discharged - (without.sim ? without.sim.discharged : 0); + if (delivered > 1) { + const worth = x.opt.solarOnly - x.bill; + let lifeKwh = 0; + for (let y = 0; y < f.batteryLife; y++) lifeKwh += delivered * Math.pow(1 - f.batteryFade / 100, y); + const rte = Math.round(((prep.ctx.battery && prep.ctx.battery.rte) || 0.9) * 100); + notes.push(`Battery: the new ${kwh(x.batteryKwh)} gives back about ${k(delivered)} kWh a year (about ${(delivered / x.batteryKwh / 365).toFixed(1)} full cycles a day)${r.existing.kwh > 0 ? ' on top of your current battery' : ''}. Each kWh it gives back saves about ${cents((worth / delivered) * 100)}: the gap between the price when it charges (or the feed-in it gives up) and the price when that power is used or sold, less what's lost on the way (${rte} % round trip). It costs ${dollars(x.fin.capex.battery)} after the rebate, about ${cents((x.fin.capex.battery / lifeKwh) * 100)} for each kWh it gives back over ${f.batteryLife} years (before interest). Saving more per kWh than it costs is a good sign.`); + } + } + const neg = negativeFeedIn(x.plan); + if (neg.all > 0) { + notes.push(`Feed-in below zero: on ${label(x.plan)} it was below zero for ${k(neg.all / 2)} hours of the year (${Math.round((neg.all / (days * 48)) * 100)} % of the time), ${Math.round((neg.midday / neg.all) * 100)} % of them between 10am and 3pm. ` + + (r.switchOff ? 'The planner assumes the panels switch off then, so nothing is paid to send power out: that needs a "curtailable" inverter.' : 'You\'ve said the panels can\'t switch off, so power sent out then costs you.')); + } + const lk = r.loads.kwh; + if (!r.check && lk.ev + lk.hotWater + lk.heating + lk.cooking < 1 && r.readingsPlan && prep.byName[r.readingsPlan]) { + notes.push(`Your bills: with nothing new, the planner prices your readings' year on ${label(r.readingsPlan)} at ${dollars(Amber.plannerBill(prep, r.readingsPlan, 0, 0))}${prep.byName[r.readingsPlan].amber ? '' : ' (today\'s rates)'}. If you were on that plan, that should be close to what your bills came to; if it's far off, check the plan's prices on the Compare tab.`); + } + if (notes.length) html += `

Rules of thumb

    ${notes.map((t) => `
  • ${esc(t)}
  • `).join('')}
`; + + // The settings this option was worked out with. + const facts = []; + if (x.solarKw > 0) facts.push(['New panels', `${kw(x.solarKw)} facing ${s.facing.toLowerCase()} at ${s.tilt}°, ${s.shading} % shade, losing ${f.solarDegradation} % a year, with ${needsCurtailable(x) ? 'a "curtailable" inverter' : 'an inverter'} of about ${kw(x.solarKw / INVERTER_RATIO)}`]); + if (x.batteryKwh > 0) { + const b = prep.batteryOf(x.batteryKwh); + const power = Math.min(15, Math.max(2.5, x.batteryKwh * ((prep.ctx.battery && prep.ctx.battery.kwPerKwh) || 0.4))); + facts.push(['New battery', `${kwh(x.batteryKwh)} usable, about ${kw(power)} in and out, ${Math.round(b.rte * 100)} % round trip, losing ${f.batteryFade} % of its capacity a year, replaced after ${f.batteryLife} years`]); + } + facts.push(['Export limit', `${s.exportLimit} kW`]); + facts.push(['Feed-in below zero', r.switchOff ? 'the panels switch off (a "curtailable" inverter); a battery soaks up what it can first' : 'the panels keep sending power out, and that costs you']); + if (x.fin && x.fin.capex.total > 0) facts.push(['Up front', capexText(x)]); + facts.push(['Power prices', `${plan.amber ? 'Amber\'s prices over your readings\' year' : `the plan's rates as at ${fmtDate(Amber.TEMPLATES_AS_AT)}`}, rising ${f.escalation} % a year`]); + facts.push(['Money', `worth is counted in today's money at ${f.discount} % a year over ${f.years} years${hardware ? `; the inverter is replaced every ${f.inverterLife} years for ${dollars(f.inverterCost)}` : ''}`]); + html += `

What it assumes

` + + `
${facts.map(([a, b]) => `
${esc(a)}
${esc(b)}
`).join('')}
` + + '

Change these under "Roof and panels", "Battery" and "Prices and rebates", then run again.

'; + $('planNumbersBody').innerHTML = html; + } + function renderPayback(x) { const r = ui.results; const notes = $('planPaybackNotes'); @@ -949,7 +1159,7 @@ 'The battery covers your home when power is dear, soaks up solar that would sell cheaply, charges from the grid in free or cheap hours, and sells when an export pays well. It knows each day\'s prices in the morning (as fixed plans and Amber\'s forecasts allow), so a real battery may do a little worse, especially on Amber\'s price spikes.', `Money: prices rise ${r.f.escalation} % a year; the battery loses ${r.f.batteryFade} % of its capacity a year and is replaced after ${r.f.batteryLife} years; the inverter after ${r.f.inverterLife}; panels lose ${r.f.solarDegradation} % a year. "Worth" is today's money at ${r.f.discount} % a year.`, r.switchOff - ? 'When a feed-in price is negative (on Amber it happens in the middle of sunny days), a battery soaks up what it can and the panels switch off for the rest, so nothing is paid to export. Untick "Panels switch off when the feed-in price is negative" if yours can\'t.' + ? 'When a feed-in price is negative (on Amber it happens in the middle of sunny days), a battery soaks up what it can and the panels switch off for the rest, so nothing is paid to export. That takes a "curtailable" inverter, one Amber can switch off remotely. Untick "Panels switch off when the feed-in price is negative" if yours can\'t.' : 'Your panels can\'t switch off, so power sent out while the feed-in price is negative costs you (a battery soaks up what it can first).', 'How keen to be also checks what happens if feed-in prices from 10am to 3pm halve, as more rooftop solar pushes them down; it uses last year\'s prices otherwise.', 'Not included: maintenance, any payments for joining a virtual power plant, the value of backup power in a blackout, or your network moving you to a different tariff after you install.' diff --git a/tests/planner.spec.js b/tests/planner.spec.js index 314d5cf..ca2d85e 100644 --- a/tests/planner.spec.js +++ b/tests/planner.spec.js @@ -127,6 +127,17 @@ test('Solar & battery: a NEM12 household gets a best setup, its year month by mo await expect(monthRows.last()).toContainText('Year'); await expect(page.locator('#planPaybackNotes')).toContainText('Paid'); + // Check the numbers: averages for the yardstick and the option, prices through the day, the settings used. + const averages = page.locator('#planNumbersBody [data-numbers="averages"]'); + await expect(averages).toContainText('Average price per kWh bought'); + await expect(averages).toContainText('Average earned per kWh sent out'); + await expect(page.locator('#planNumbersBody [data-numbers="bands"] tbody tr')).toHaveCount(5); + await expect(page.locator('#planNumbersBody [data-numbers="assumes"]')).toContainText('Export limit'); + // The option's bill there is the month table's year. + const yearCells = await monthRows.last().locator('td').allTextContents(); + const billCells = await averages.locator('tbody tr').last().locator('td').allTextContents(); + expect(billCells[billCells.length - 1].trim()).toBe(yearCells[yearCells.length - 3].trim()); + // Tapping another option shows its year instead. const second = page.locator('#planOptionsTable [data-plan-option="1"]'); const secondPlan = (await second.locator('td').nth(1).textContent()).trim(); @@ -198,6 +209,39 @@ test('Solar & battery: fixing the panels, the battery and the plan', async ({ pa expect(errors).toEqual([]); }); +test('Solar & battery: on Amber, a curtailable inverter and the numbers to check', async ({ page }) => { + test.setTimeout(150000); + const errors = []; + page.on('pageerror', (e) => errors.push(e.message)); + await importYear(page); + await page.click('#tab-plan'); + await expect(page.locator('#planInputs')).toBeVisible(); + await page.selectOption('#planSolarMode', 'fixed'); + await page.fill('#planSolarKw', '10'); + await page.selectOption('#planBatteryMode', 'none'); + await onlyPlans(page, (t) => t.startsWith('Amber')); + await page.click('#planRun'); + await expect(page.locator('#planResults')).toBeVisible({ timeout: 120000 }); + // Amber's feed-in price goes below zero and the panels are assumed to switch off: that takes a curtailable inverter. + await expect(page.locator('#planRecommendation')).toContainText('What to ask installers for: 10 kW of panels with a "curtailable" inverter of about 7.5 kW.'); + await expect(page.locator('#planRecommendation [data-curtail]')).toContainText('Curtailable inverter needed.'); + await expect(page.locator('#planDetailBody')).toContainText('Curtailable inverter needed.'); + await expect(page.locator('#planNumbersBody [data-numbers="bands"] thead')).toContainText('Feed-in below zero'); + const thumb = page.locator('#planNumbersBody [data-numbers="thumb"]'); + await expect(thumb).toContainText(/about \d\.\d kWh a day for each kW of panels/); + await expect(thumb).toContainText(/Each kWh the new panels make is worth about -?\d+\.\d c/); + await expect(thumb).toContainText(/Feed-in below zero: on Amber .* below zero for [0-9,]+ hours/); + await expect(page.locator('#planNumbersBody [data-numbers="assumes"]')).toContainText('a "curtailable" inverter of about 7.5 kW'); + + // Panels that can't switch off: no curtailable inverter assumed, and the note says what one would avoid. + await page.locator('#planInputs summary', { hasText: 'Roof and panels' }).click(); + await page.uncheck('#planSwitchOff'); + await page.click('#planRun'); + await expect(page.locator('#planRecommendation [data-curtail]')).toContainText('A "curtailable" inverter would help.'); + await expect(page.locator('#planRecommendation')).toContainText('What to ask installers for: 10 kW of panels with an inverter of about 7.5 kW.'); + expect(errors).toEqual([]); +}); + test('Solar & battery: a chosen plan to compare against keeps the yardstick; "whatever\'s cheapest" follows the plans ticked', async ({ page }) => { test.setTimeout(150000); const errors = []; diff --git a/tests/unit-time.js b/tests/unit-time.js index a5c951e..8c472ac 100644 --- a/tests/unit-time.js +++ b/tests/unit-time.js @@ -78,7 +78,7 @@ const clockA = Amber.clockPartsForItem(clockItem, vicLocal, 'VIC'); const clockB = Amber.clockPartsForItem(clockItem, vicLocal, 'VIC'); assert(clockA === clockB, 'clock parts are cached on the usage item'); assert(clockA.hours === 17, 'cached Melbourne DST hour is 17'); -assert(Amber.APP_VERSION === '1.58', 'app version is 1.58'); +assert(Amber.APP_VERSION === '1.59', 'app version is 1.59'); assert(Amber.DEFAULT_AMBER_CONNECTION_CENTS === 125.367 && Amber.PREVIOUS_AMBER_CONNECTION_CENTS === 116.787, 'Amber daily connection defaults (2026-27 and 2025-26)'); assert(Amber.DEFAULT_AMBER_SUBSCRIPTION_CENTS === 82.181, 'Amber subscription default'); assert(Amber.DEFAULT_AMBER_DEMAND_CENTS === 43.431 && Amber.PREVIOUS_AMBER_DEMAND_CENTS === 42.345, 'Amber demand defaults (2026-27 and 2025-26)');