=== Gradespace Core ===
Contributors: gradespace
Tags: business, settings, staff access, setup wizard, branded email
Requires at least: 6.8
Tested up to: 7.1
Requires PHP: 8.0
Stable tag: 0.8.7
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

The free back office every Gradespace kit runs on: settings in plain words, a Today screen, staff access, branded emails, a setup wizard and updates.

== Description ==

Gradespace Core is free. It is the shared back office for the Gradespace kits for WordPress (Clinic, Church and Music):

* **Settings in plain words**: your business name, logo, phones, address and opening hours, entered once and used everywhere.
* **A Today screen** with what needs doing, and count bubbles in the menu.
* **Staff access**: roles for your team, each seeing only what their job needs (choosing exactly what each role can do is a Pro feature).
* **Branded emails** with an on/off switch for each one and a log (the log is a Pro feature).
* **Sign-in protection** and a security log, a branded sign-in page, cookieless site stats (Pro), a cookie banner, and starter legal pages.
* **A setup wizard** that gets a new website ready in minutes.
* **Your kit licence** (Settings → Gradespace licence) and **automatic updates** for Core, your kit plugin and its theme.
* **Payments for kits that take them**: deposits on bookings and gift vouchers (Pro), paid through Stripe directly or your WooCommerce checkout, with refunds and a list of every payment. Customers pay on Stripe’s or the shop’s own secure page; card details never touch your website.

Core itself needs no licence key. The key you receive when you buy a kit goes in Settings → Gradespace licence and covers that kit’s plugin and theme.

Kits come in three parts that work together, installed in this order:

1. **Gradespace Core** (free, this plugin).
2. **The kit plugin** (Gradespace Clinic, Church or Music).
3. **The kit theme** (the matching Gradespace theme).

== Installation ==

You need a WordPress website (WordPress 6.8 or later, PHP 8.0 or later) and to be signed in as an administrator.

First, download the three files from your licences page, https://thegradespace.com/account/licences (sign in with the email address you bought with). Leave them as .zip files: WordPress wants them that way. Gradespace Core on its own is also free to download from https://thegradespace.com/wordpress-kits.

Then install them **in this order**:

1. **Gradespace Core** (gradespace-core-….zip). In WordPress, go to **Plugins → Add Plugin → Upload Plugin**, choose the file, click **Install Now**, then **Activate Plugin**. If the setup wizard opens, leave it for now: you’ll come back to it in step 4.
2. **Your kit plugin** (for example gradespace-clinic-….zip). The same again: **Plugins → Add Plugin → Upload Plugin**, choose the file, **Install Now**, then **Activate Plugin**.
3. **Your kit theme** (for example gradespace-clinic-theme-….zip). Go to **Appearance → Themes → Add Theme → Upload Theme**, choose the file, click **Install Now**, then **Activate**.
4. **Run the setup wizard.** It opens by itself; if it doesn’t, click **Continue setup** on the Dashboard. A few questions about your business, then starter pages and a menu. You can run it again later from **Settings → Run setup again** in your kit’s menu (Clinic, Church or Music school).
5. **Add your licence key.** Go to **Settings → Gradespace licence**, paste the key from your receipt email and click **Activate**. That switches on updates, and Pro or Agency features if your licence includes them.

New versions then arrive in **Dashboard → Updates** like any other plugin or theme.

(Older versions of WordPress say “Add New Plugin” and “Add New Theme”, or just “Add New”.)

A step-by-step guide with pictures, and staff guides to download as PDFs: https://thegradespace.com/wordpress-kits/getting-started

== Moving to another website ==

Your licence covers one live website at a time (an Agency licence covers five). To move it to a new website:

1. **Free it on the old website.** Go to **Settings → Gradespace licence** there and click **Deactivate on this website**. If the old website has already gone, sign in at https://thegradespace.com/account/licences and click **Remove site** next to it instead.
2. **Activate it on the new website.** Install as above, then paste the same key in **Settings → Gradespace licence** and click **Activate**.

Nothing you saved is deleted when you deactivate a key.

**Local and staging copies don’t count.** A copy on your own computer or a test address (localhost, anything ending in .local or .test, or a staging. or dev. sub-domain) can use your key without taking up your website. So you can build and try things on a copy, then go live.

== Frequently Asked Questions ==

= How do I set up online payments? =

Only kits that take deposits or sell gift vouchers use payments. Go to your kit's **Settings → Payments**. For Stripe, paste your publishable and secret keys (Stripe dashboard → Developers → API keys), add a webhook endpoint with the address shown there, and paste its signing secret. Use test keys (sk_test_…) first and make a test booking or voucher; switch to live keys when you're happy. Or choose "Your WooCommerce checkout" to use the payment methods your shop already has.

= Do I need a licence for Gradespace Core? =

No. Core is free. The licence key is for your kit (Clinic, Church or Music) and covers its plugin and theme.

= Where is my licence key? =

In your receipt email. You can also look it up at https://thegradespace.com/account/licences with the email address you bought with.

= What happens before I add a key? =

The kit runs in Starter mode so you can try it, but it doesn’t get updates or Pro features until you add your key.

= WordPress says the key is already in use on all its websites. =

Your licence is still active on another website. Deactivate it there (Settings → Gradespace licence), or remove that website at https://thegradespace.com/account/licences, then click Activate again.

= How do updates work? =

Once your key is active, new versions of Gradespace Core, the kit plugin and its theme appear in Dashboard → Updates. Click Update, as you would for any plugin. Updates are included for life.

= How do I remove the sample content? =

Go to Site Settings → Sample content (administrators only). It lists what the setup wizard added as samples and removes it with one button: sample pages, posts and records go to the Bin, and sample photos are deleted unless something on your website still uses them. Samples you've edited are kept unless you tick "Also remove sample items I've edited".

= Is there a guide for my staff? =

Yes, one for each kit, as a PDF: https://thegradespace.com/wordpress-kits/getting-started

= Where can I get help? =

Email support@thegradespace.com with what you were doing, what you expected and a screenshot, and we’ll help.

== Copyright ==

Gradespace Core, Copyright Gradespace Ltd. Licensed under the GPLv2 or later.

== Changelog ==

= 0.8.7 =
* **Retired designs** (Site Settings → Design & colours): a theme can stop offering a design without changing any website that uses it. It names the design in its design-styles support (`'retired' => array( 'dark' => 'a line for the owner' )`, or a plain list of slugs; also the `gsc_designs_retired` filter). The design's style file stays, so a website that chose it renders exactly as before. Its card shows only while it is the saved choice, as "Name (retired)" with the theme's line (or "No longer offered for new choices. Your website keeps it until you choose another design."); once another design is saved, it is gone. Saving can keep a retired design that is already saved, but never choose one anew.
* Demo websites: a visitor can't pick a retired design, and a demo cookie that holds one (or a design the theme no longer has) gives way to the website's usual look, without the "You're seeing your own choice" notice (`gsc_demo_design_cookie_choice()`).
* New: `gsc_designs_offered( $keep )` (the designs to offer, keeping the saved one) and `gsc_designs_retired()`. `gsc_designs()` still lists every design, retired ones included, so nothing that renders a design changes.

= 0.8.6 =
* **Site Settings → Sample content** (administrators): what the setup wizard added as samples, by kind ("12 sample pages, 30 sample photos, 8 sample sermons…"), and a **Remove sample content** button with a confirmation in Core's dialog. Sample pages, posts, products and records go to the **Bin** (restorable as usual); sample photos are deleted only when nothing left on the website uses them (a featured image, a gallery or picture field, block content by ID or by file, the logo or site icon); sample categories and tags only when nothing else uses them; a Photo credits page only once no sample photo is left. Samples changed after they were added are kept unless "Also remove sample items I've edited" is ticked. Orders, the owner's own content and records never carry the marker and are never touched. Afterwards: a summary of what went and what stayed, with links to the Bin. It runs in batches over AJAX with a count (without JavaScript, in rounds: "Carry on removing"), is logged in the security log ("Sample content removed"), and is switched off on demo websites. Without the kit theme's Site Settings menu it sits in the business menu.
* **One marker for samples**: post meta `_gsc_sample` = "kit|import time" (`gsc_mark_sample()`, `gsc_mark_sample_credits()`), term meta (`gsc_mark_sample_term()`, with a fingerprint of the name and description to spot edits), and recorded photo lists (`gsc_mark_sample_option()`, pruned when photos go). `gsc_import_sample_photo()` marks the photos it adds. The setup wizard settles the import time after each importer, so changes an importer makes to its own samples don't count as edits. Older markers are recognised: `_gsc_sample_source` and the kits' own `_<prefix>_sample` flags (`gsc_sample_legacy_keys`), "sample-…" seed addresses (`gsc_sample_legacy_seed_keys`), the themes' photo lists and Photo credits pages (`gsc_sample_legacy_options`), and named terms (`gsc_sample_legacy_terms`).
* Kits name their kinds with the `gsc_sample_kinds` filter (`_n_noop()` or a callable); also `gsc_sample_never_types`, `gsc_sample_photo_in_use`, `gsc_samples_time_budget` and the `gsc_samples_removed` action. WP-CLI: `wp gradespace samples list` and `wp gradespace samples remove [--edited]`.
* The wizard's Starter content step says the samples are marked and links to Sample content. Running setup again adds the samples again.

= 0.8.5 =
* The kits' category screens (Subjects, Sermon series, Specialisms, Gallery tags…) have a search button that says what it searches ("Search subjects"), not WordPress's "Search Tags" or "Search Categories".
* The Publish box on the kits' record screens (students, learners, visits, enquiries…: `gsc_record_post_types`) shows dates day first, "7 Oct 2026 at 14:05", and its date boxes under Edit run day, month, year (`gsc_record_date_formats` filter).
* Locations without a colour field (Driving's areas covered, Trades' service areas) no longer get a Colour column on their list, and a page's "belongs to" help no longer mentions a colour.

= 0.8.4 =
* **Help & manual**: the kit's menu ends with a Help & manual link, and Today has a Help & manual button, both opening the kit's user manual on thegradespace.com/docs/<kit> in a new tab. Kits name their manual with the `docs` key of the `gsc_menu` filter (without one, the only registered kit's manual); `gsc_docs_url()` gives the address, and the `gsc_docs_url` filter changes or hides it (return '').
* Settings tabs for Pro features show their **Pro** badge again in Starter mode, instead of its HTML as text ("Holds <span class=…>Pro</span>"); the save bar and the saved message name the tab without the badge (`gsc_settings_tab_name()`).

= 0.8.3 =
* **Choose image / Change image work again** (Settings → Business: logo, sign-in page photo, share picture; the setup wizard's Business step). The buttons looked for the media library before WordPress had loaded it.
* **Settings → Email shows every kit's recipient lists** (Church: Copy visit requests to, Send prayer requests to; Music: Send lesson requests to; shop orders; contact form). The lists were registered after the Email tab was built, so they never showed (`gsc_notify_lists()`).
* **Sample photos in batches**: the setup wizard's Starter content step adds its content one part per request, with a progress list, and slow parts (the sample photos) in batches inside the host's time limit, so a 30-second limit no longer ends in a critical error. Without JavaScript the step carries on where it stopped each time "Save and continue" is pressed. New for importers: `gsc_wizard_time_left()`, `gsc_wizard_more()`, `gsc_wizard_tally()`, and `gsc_import_sample_photo()`, which adds a photo once and finds it again by its source key (or, for older imports, its file name), so running setup again, or after an interrupted run, never makes copies.
* The wizard's All done list shows only what that run did (it could repeat "Added 8 services" from an earlier run); the Welcome list is numbered as in the step bar.
* The sticky save bar no longer covers the field being filled in. On wide screens the settings tabs wrap onto a second row instead of hiding the last ones (Privacy, Redirects, Licence) off the edge.
* "Saves the “Your venue” tab only." keeps the tab's name as written; no doubled "(optional) (optional)" when a label already says it.
* Plain category screens for the kits' own lists: no Slug box or column, Parent is "Part of", plain help without WordPress's Jazz/Bebop example. Private records lose "Visibility: Public", and records (patients, students, clients, enquiries…; `gsc_record_post_types`) lose "Status: Published".
* The update notice names products ("Gradespace Clinic", "Gradespace Clinic theme"), not their slugs; the licence key box shows the real shape, GS-XXXX-XXXX-XXXX-XXXX.
* Wording: gift voucher help names the kit's own menu; Country calling code help says what blank means; "This map needs your permission…" (not "This the map…"); Staff access says Users → Add User; readmes use WordPress 7.1's menu names.
* Booking calendar day view: bookings outside opening hours sit above the closed-hours shading and read clearly. Switching view while today is on screen starts the new view at today (after Month, Week → Next no longer jumps a month).
* Legal pages with a shop: WooCommerce's untouched "Refund and Returns Policy" draft is filled in as "Returns and refunds" instead of a second returns draft beside it (and WooCommerce points to Core's page when Core makes one).
* The contact sheet's WhatsApp link adds the country code (07700… → wa.me/447700…); a location's "Add a …" reads "Add an …" before a vowel; the location Email help no longer mentions visit requests on every kit; a service's "Minutes to pay" help says what 0 or blank means.
* Design & colours: the palette help mentions the dark design only on themes that have one.

= 0.8.2 =
* **Keep the menu at the top when scrolling** (Site Settings → Design & colours → Header): the header with the name and menu stays in view as visitors scroll, for every Gradespace kit theme. Preview shows it before saving. When it's on the page gets `gsc-sticky-header` (off: `gsc-static-header`), the header (`header.wp-block-template-part`) sticks with an opaque background from the design's own page colour (`--gsc-sticky-bg` to change it), gains a soft shadow once scrolled (`is-scrolled`, set by a small script with IntersectionObserver; no transition with reduced motion), sits below the admin bar on desktop and at the very top on phones (where WordPress lets the admin bar scroll away), and in-page links land below it (`scroll-margin-top` from the header's real height). Notice bars a theme names (`sticky_away`) scroll away and only the main bar stays. The demo bar sits at the bottom, so it never covers the header.
* **Existing websites don't change**: until an owner chooses, the setting follows the theme. Themes whose header already stuck (Clinic, Church, Music: `'sticky_header' => 'own'`) start on and keep their own look; switching it off lets that header scroll away. The other themes start off.
* **New websites**: the setup wizard's starter step has "Keep the menu at the top when scrolling", ticked, which switches it on (never over a choice the owner already made).
* Demo websites keep a visitor's own choice with their design and palette (older design cookies keep the site's setting).

= 0.8.1 =
* Updates: Gradespace downloads are fetched by Core itself, so the updates screen no longer shows the download address (it carries a signed one-hour key). A failed download shows a plain message without the address. Other plugins' updates are unchanged.

= 0.8.0 =
* **Multi-booking** in the booking engine (engine version 1.1.0): one customer books several times or dates in one go, as one order. Off by default: a booking type opts in with `'multi' => [ 'max' => N, 'shapes' => [ … ] ]` in `gsc_booking_register()`, so types that don't ask (Clinic, Music) are unchanged.
* **All or nothing**: `gsc_booking_create_group( $type, $items, $customer, $opts )` takes the type's lock once and books every item inside one database transaction, each with the same checks as a single booking (the kit's own booking function when it has one). If any item has gone, nothing is kept and the answer (409) names every item that clashed, with free times that day. `gsc_booking_group_quote()` checks and prices without booking.
* **Shapes** (`gsc_booking_shape()`): a consecutive date range (each day with its own time or part of the day), separate dates, a weekly series until a date (skipping closures and the type's own skip dates, e.g. half terms; only the first date needs to be inside the booking horizon), times against a package balance, and several people at the same visit on different staff ("anyone" picks a different free person for each). Pure helpers `gsc_booking_dates_between()` and `gsc_booking_weekly_dates()`.
* **One order**: a booking group (new table `gsc_booking_groups`) has one reference, the customer, a total, one deposit or payment (the sum of each item's deposit, one Stripe or WooCommerce charge), a status and a history. Every item stays an ordinary booking (`_gsc_group`), so calendars, staff screens and reminders keep working; reminders still go per item. One confirmation email lists the items, with one calendar file holding one event per item, and one link to the customer's page for the group (`?gsc_group=<key>`).
* **Waiting for payment together**: every item of a group with a deposit waits with the same hold and is released together if it isn't paid; paying confirms them all with one email; a late payment is kept only if every time is still free.
* **Change the whole group or one item**: `gsc_booking_group_cancel()` and `gsc_booking_group_move()` (whole days for everything, or a new time for one item; all or nothing), from Business → Booking groups and from the customer's page. Refunds follow what was cancelled: each item refunds its share of the payment, by the deposit cancellation setting for customers; staff choose.
* **REST**: `POST gsc/v1/booking/{type}/group` (action book, quote or check; a shape with its parameters, or a list of items), `GET gsc/v1/booking/{type}/group/{key}`, and `/cancel` and `/move` with the link key.
* **Business → Booking groups**: each group as one row with its items, cancel all or one (with or without a refund), move all by whole days, paid in person.
* The booking lock can be taken again by the same request (it counts, so only the outermost release frees it).
* Privacy: groups are in WordPress's personal-data export and erase.

= 0.7.2 =
* The cookie banner and the floating cookie button show on marketing pages only. App pages leave them out: My account, the basket and checkout, a booking's manage link and the demo checkout. These pages use essential cookies only. The settings dialog stays, so a "Cookie settings" link still opens it. Kits add their own pages with the `gsc_consent_app_page` filter.

= 0.7.1 =
* **Demo checkout** (public demo websites only, `GSC_DEMO_MODE`): a payment driver `demo` so visitors to the kit demos can try deposits and gift vouchers without real money. "Pay" opens a page on the website in the business's colours, labelled "Demo checkout: no real payment is taken", with the amount and two buttons, "Pay (demo)" and "Cancel"; there are no card fields. "Pay (demo)" completes the payment the normal way, so the booking is confirmed, the voucher is issued and emailed (the demo's email log catches every email), and refunds work. Presses of "Pay (demo)" are limited per visitor and for the whole site, and the nightly reset clears the payments. On a demo website no other payment provider can take payments, Stripe can't be reached, and Settings → Payments is read-only. Customer websites never load any of it.

= 0.7.0 =
* **Payments** (new module `pay`, engine version 1.0.0, off until a kit switches it on): one way for any kit to take money without card details ever touching the website. `gsc_pay_charge( [ amount (pence), currency, description, object_type, object_id, name, email, metadata, success_url, cancel_url, idempotency_key, expires_in ] )` returns the provider's hosted payment page; the same request again while that page is open gives the same page (no second charge). Every payment is a row in a new ledger table (`gsc_payments`: reference, driver, test/live, what it's for, amount, refunded, status, customer, the provider's ids, a short history; never card or bank details). Hooks `gsc_pay_paid`, `gsc_pay_failed`, `gsc_pay_expired`, `gsc_pay_refunded` (each change happens once, however many times the provider repeats itself); refunds of all or part with `gsc_pay_refund()` (one at a time under a lock); drivers with `gsc_pay_register_driver()`.
* **Stripe, directly** (no shop needed): Settings → Payments takes the publishable key, the secret key and the webhook signing secret (both kept as secrets, never shown again; or `GSC_STRIPE_SECRET` / `GSC_STRIPE_WEBHOOK_SECRET` in wp-config.php) and says whether it's in test or live mode, with a "Check the connection" button. Stripe Checkout Sessions through the API (no SDK), automatic tax off unless switched on, pounds by default. The webhook (`/wp-json/gsc/v1/pay/stripe/webhook`) checks Stripe's signature (HMAC SHA-256, five minutes' tolerance) before reading anything; refused messages are written to the security log and limited per address. If a webhook is late, coming back from the payment page asks Stripe directly.
* **Or your WooCommerce checkout**: "Take payments through: your WooCommerce checkout" makes a pending order with one line for a hidden product (private, can't be bought from the shop), named after what is being paid for, and sends the customer to the order-pay page with any of the shop's payment methods. Paid on payment complete or when staff mark the order Processing (bank transfers: the time is held longer); refunds go through WooCommerce, and refunds made in the order screen are counted. Works with order tables (HPOS).
* **Deposits** (Pro): a kit lets a booking type take deposits with `gsc_pay_deposits_register( $type, [ item_post_type, price, description, contact ] )`. Each service then has a Deposit panel: none, a fixed amount, a percentage of the price or the full price, and the minutes to pay. Online bookings wait as "Waiting for payment" with their time held, the REST booking answer has `pay_url`, and payment confirms the booking (or makes it a request). Unpaid holds run out on time and the checkout is closed; a payment that arrives after that keeps the booking if the time is still free, otherwise it's refunded automatically. When the customer cancels in time: refund automatically, keep, or let staff decide (Settings → Payments → Deposits); when staff cancel, they decide. Staff can mark a waiting booking "Paid in person". Staff bookings never ask for a deposit.
* **Gift vouchers** (Pro, any kit): blocks "Buy a gift voucher" (preset amounts, any amount between a minimum and maximum, or a named service from the kit) and "Gift voucher balance". Paid vouchers are emailed (branded, with the code and a printable view) to the person they're for or to the buyer, with a receipt and a note to staff. Business → Gift vouchers: find by code, email or name, take an amount at the till, email again, print, cancel, issue one by hand (optionally recorded as paid at the desk), full history. A voucher can pay all or part of a deposit when booking online (a setting). Codes are 16 characters without look-alikes; lookups are limited per address and repeated wrong codes are locked out and logged. Expired vouchers are marked daily and a reminder goes out before expiry. Without Pro, existing vouchers are still honoured.
* **Business → Payments**: totals for the month, filters, each payment's history, Refund (all or part) with a confirmation, "Keep the deposit", "Paid in person", and a CSV export of the latest records. Today shows deposits waiting for a decision.
* Staff abilities "See payments and take vouchers" and "Give refunds and issue vouchers" (administrators by default). Payments and vouchers are in WordPress's personal-data export and erase (names and emails removed; amounts and dates kept as financial records).
* Booking engine: the REST "book" answer can be extended (`gsc_booking_book_response`). Security log: event types can be labelled by modules (`gsc_security_event_labels`).

= 0.6.0 =
* **Booking engine** (new module `booking`, off until a kit switches it on): one engine for every kit that takes bookings. A kit describes its bookings once with `gsc_booking_register( $type, [ post_type, meta key map, statuses, resources named|pool, resource_ids, duration, buffers, capacity, hours, settings… ] )` and keeps its own post types and meta keys (no data moves). The engine works out free times (business hours ∩ the staff member's own hours − days off and blocked times − closures − other bookings with their gaps before and after, notice period, booking horizon, start-time step), books under a database lock with a re-check (two people can never take the same time), gives "anyone" to the least busy person, confirms at once or as a request, keeps statuses (and a history when asked), moves and cancels bookings, makes the customer's manage/cancel link, sends reminders the day before (one hourly job for every kit), and decides who is emailed when — the kit writes the emails, or the engine's plain ones are used. Functions `gsc_booking_slots()`, `gsc_booking_days()`, `gsc_booking_create()`, `gsc_booking_set_status()`, `gsc_booking_move()`, `gsc_booking_by_token()`, `gsc_booking_can_cancel()`, `gsc_booking_is_free()`, `gsc_booking_bounds()`…; hooks `gsc_booking_validate`, `gsc_booking_pick_resource`, `gsc_booking_slots`, `gsc_booking_notify`, `gsc_booking_saved`, `gsc_booking_created`, `gsc_booking_status_changed`, `gsc_booking_moved`, `gsc_booking_reminder_sent`; clock `gsc_booking_now` (tests and demos).
* Ready for deposits later: a booking can wait for payment (`pending_payment`, filter `gsc_booking_payment`, action `gsc_booking_payment_needed`, `gsc_booking_mark_paid()`); the time is held until the hold runs out, then freed. Nothing takes payments yet.
* REST API for booking types that ask for it: `gsc/v1/booking/{type}/days`, `/slots`, `/book`, `/cancel` (limited per visitor address, never cached, Core's spam guard on bookings).
* **Admin calendar component** `gsc_calendar_render()`: day (a time grid, one column per staff member), week and month views, filters, legend, "+ Add" links, works on phones.
* **Calendar files** (new module `ics`, on): one writer for every kit, `gsc_ics_event()`, `gsc_ics_calendar()`, `gsc_ics_text()`, `gsc_ics_temp_file()`, `gsc_ics_send()`. Long lines are folded at 75 octets as the standard asks (calendar apps show the same text).
* **Version gate for kits**: `gsc_requires( 'core'|'booking', $version )`, `gsc_engine_version()`, `gsc_engine_versions` filter. A kit can say "Requires Core: x.y" in its header; the update list carries it (`requires_core`), and a kit update that needs a newer Core waits, with "Update Gradespace Core first" on its row, until Core is updated.
* Date helpers `gsc_is_ymd()` and `gsc_local_ts()`.

= 0.5.1 =
* Security tab: Recent activity is shown in pages (10, 25 or 50 rows) instead of the last 100 in one list; the chart's 'Show as a table' scrolls in a box with its headings pinned.
* New filter `gsc_time_label` (label, H:i time, DateTimeImmutable): a site can write times its own way everywhere Core and the kits show them (e.g. “9:30 am”, “11:00 am” instead of “9:30am”, “11am”). Nothing changes without it.

= 0.5.0 =
* **Sign-in page photo** (Settings → Business → Sign-in page): an optional photo behind the panel beside the sign-in form, darkened with the brand colour so the words stay easy to read. On phones it becomes a short banner. None: the brand colour as before.
* **Line at the bottom of the panel** (same card): a small line such as your full name and area. Blank: the kit's usual line.
* The panel can list **what staff look after here**, each with an icon from Core's set. Filter `gsc_login_panel` gains `features` (a list of [icon name, text]), `photo` (an image URL) and `foot` (plain text); `facts`, `title`, `lead` and the Agency white-label words work as before. New helper `gsc_login_panel()`.
* **Sign-in switch**: new filter `gsc_login_switch` (a list of label, url, current). With two or more places, a segmented control (“Where do you want to sign in?”, the current one marked) shows above the sign-in form, e.g. “Website editor | Church app”.
* “← Back to website” now sits above the form on every screen (sign in, lost password, new password), so it is there on phones too; it left the panel's list.
* Icons: `layout` (page layout) and `cart` (shopping cart).
* Settings: a picture field with help text now shows its name and help (e.g. Search and sharing → Share picture).

= 0.4.0 =
* **Demo mode** (new module `demo`, for public demo websites only): on only when wp-config.php has `define( 'GSC_DEMO_MODE', true );` (optional `GSC_DEMO_KIT`), and no effect anywhere else.
  * **No email ever leaves the website**: every email (branded, WordPress, WooCommerce) is answered as sent without sending and written to the email log as **“Demo — not sent”**; PHPMailer is a dead end as a backstop. The email log says it's a demo.
  * **“Look around the admin”**: a button on the sign-in page and in the demo bar, and `/demo-login`, sign a visitor in as the demo staff member (login `demo`, role **Demo staff** with every staff ability of the kit except deleting records for good) with no password, behind a signed token and a limit per address. Password sign-in, password resets and application passwords are refused for that account; a demo visit lasts four hours.
  * **Guard rails** for the demo account: no plugins, themes, users, roles, profile, passwords, licence, exports, imports, uploads, site options, tools, file editor, setup wizard, staff access, email-log clearing or IP blocking (403 “Not in the demo”, and those menu items are hidden). Settings screens open, but Save says **“Saving is switched off in the demo”**. The website's public pages, menus, templates, styles, products and the kit's public content can't be changed or deleted (REST 403, classic screens 403, and a backstop that keeps the stored fields); new public content is saved as a draft; categories and other terms can be added but not renamed or deleted. Private staff records (appointments, patients, prescriptions, lessons, students, visits, prayer, messages, orders, stock, the till) work in full.
  * **Design & colours** in the demo is kept per browser (a session cookie): a visitor's choice shows on the demo website for them only, with “Go back to the usual look”. New filter `gsc_design_saved`.
  * **Demo bar** (“Demo website — resets every night · Look around the admin · Buy this kit”, linking to thegradespace.com/wordpress-kits/<kit>), fixed at the bottom of the website and at the top of the admin, hidden for the rest of the session with ×; `noindex, nofollow` (meta and `X-Robots-Tag`), robots.txt disallows everything, no sitemaps, comments off.
  * **Limits**: form posts and REST writes from one address (bookings, contact, Plan a visit, cart and checkout) are limited to 40 an hour (200 for the shared demo account) and 1,500 an hour for the whole site, on top of each form's own limits and honeypot.
  * **Nightly reset**: `wp gradespace demo reset --kit=<clinic|church|music> --yes` empties the website (content, visitors' accounts, the kits' tables and settings; keeps administrators, the licence and the sample photos) and runs the kit's own setup wizard again with sample answers and every starter-content importer, then the demo's sample records dated from today, the demo account, the design and the licence (`GSC_LICENCE_KEYS`). Idempotent; a few seconds. `wp gradespace demo status` reports the kit, last reset, licence, demo account, emails and counts. Hooks for a deployment's sample business and records: `gsc_demo_profile`, `gsc_demo_before_setup`, `gsc_demo_seed`; also `gsc_demo_store_url`, `gsc_demo_denied_caps`, `gsc_demo_blocked_actions`, `gsc_demo_excluded_abilities`, `gsc_demo_keep_options`, `gsc_demo_keep_posts`.
* `gsc_demo_on()` helper (always available).

= 0.3.4 =
* Plugins screen: no more “This plugin has not been tested with your current version of WordPress” on point releases. “Tested up to 7.1” now covers 7.1.1, 7.1.2 and so on, as it does for plugins from WordPress.org.

= 0.3.3 =
* Settings, licence and Design & colours screens use the full width of the screen (they stopped at about 1,100px, so the tab bar scrolled on wide screens). Design cards and colour palettes spread across one row.

= 0.3.2 =
* Added readme with installation steps.

= 0.3.1 =
* Sign-in credit: **Pro and Agency** licences can now choose to hide the “Website by Gradespace” line under the staff sign-in form (a switch on Settings → Licence; off = the credit is shown). Starter (and no key) always shows it. New feature `core:hide_credit` (Pro).
* The white-label panel heading and text stay **Agency** only (`core:white_label`). A Pro licence sees the credit switch in a “Sign-in page” card, with an Agency upgrade card for the panel words; saving as Pro keeps any stored words untouched (and they aren't applied).
* Upgrade cards show the kits' launch prices (Starter £69, Pro £139, Agency £389, VAT-inclusive), so “Upgrade to Pro for £70” etc.

= 0.3.0 =
* **Licences** (new required module `licence`): Settings → Licence (also Settings → Gradespace licence in WordPress's own menu) with one key per installed kit: Activate, Recheck, Deactivate, a status card (tier, websites used/allowed, lifetime, last checked), plain messages for every server answer (invalid, limit, revoked, product mismatch, server unreachable), buy and upgrade links (upgrades go through `admin-post.php` so the key never appears in a page). Keys are kept in `gsc_licences` (not autoloaded), shown as their last four characters, administrators only. Kits register with `gsc_register_kit()`; `gsc_licence_tier()`, `gsc_has_feature()`, `gsc_licence_upsell()`, `gsc_licence_rest_check()`, `gsc_licence_require()`, `gsc_licence_badge()`; filters `gsc_licence_tier`, `gsc_has_feature`, `gsc_licence_dev_site`; constants `GSC_LICENCE_API`, `GSC_LICENCE_KEYS`, `GSC_LICENCE_DEV_SITE`.
* Paying customers are never locked out: a verified tier is kept and re-checked weekly; an unreachable server (or rate limit, or server error) keeps the last known tier; only a definite revoked/invalid answer downgrades. Test copies (localhost, *.local, *.test, staging.*, dev.*) use the key's tier without taking up a website.
* Without a key a kit runs in Starter mode for evaluation, with a notice ("add your licence key to get updates") and no updates.
* Pro modules need a Pro (or Agency) kit licence: Staff access matrix, Email log and Site stats (visitors) show an upgrade card instead, keep a "Pro" badge in the menu, and their handlers answer 403. Nothing is deleted: the saved access keeps applying, emails are still logged and visits still counted, the staff list and the security tab stay available.
* Agency: white-label sign-in (own panel heading and text; hide the new "Website by Gradespace" credit under the sign-in form).
* Updates come from the licence API's per-site manifest (site address plus the kit keys in `X-Gradespace-Keys`); a `null` download link shows "Add your licence key to update" under the plugin row and as a notice on Themes and Dashboard → Updates; just before installing, the manifest is fetched again so signed download links never expire mid-update. `GSC_UPDATES_MANIFEST` still overrides (a local file only offers kit updates with a verified key). View details reads `last_updated` and `homepage`.
* GSCookie: when it's active, Core's cookie banner steps aside, gated embeds and `gscConsent` follow GSCookie's choices (new `consent-bridge.js`), the cookie policy text refers to it, and Settings → Privacy says so. Core's admin styles for `.gsc-card`, `.gsc-grid`, `.gsc-pill`, `.gsc-status` are scoped to `body.gsc-ui` (Core and kit screens, the Dashboard), and GSCookie's screens are no longer mistaken for Core's.
* Settings tabs can print their own forms (`'form' => false`).

= 0.2.1 =
* Contact form messages: review before deleting is now the default. A new setting (Settings → Contact → "When messages are due": review or delete automatically, `forms_retention_mode`) keeps old messages until someone deletes them; Messages → Due for review lists the ones older than the review period, and administrators delete the ticked ones with "Delete selected" after typing DELETE. Sites that want the old nightly deletion choose "Delete them automatically". The contact form's small print follows the choice.
* Email log: administrators can delete ticked entries, clear the "not sent" entries or clear the whole log (each asks first). The failed-emails count, the Settings bubble and the Today warning update straight away.
* Dialogs: `gsc_register_bulk_confirm()` asks before a bulk action on a list screen, optionally with a word to type.
* Post fields: a field panel can be kept to people with a capability (`cap`): others don't see it, can't change it and don't get its values from the REST API. New field type `posts` (several posts, ticked) with `gsc_field_ids()`.

= 0.2.0 =
* New modules for the Church and Music kits, off until a kit (or site) turns them on:
  * **Locations**: many locations with a colour pair each, their own pages, "belongs to" pages and a location binding source.
  * **SEO**: meta descriptions, link previews (Open Graph/Twitter) and structured data; steps aside when an SEO plugin is active.
  * **Redirects**: 301s from old addresses, managed in Settings → Redirects.
  * **Share buttons** block.
  * **Fields**: declarative post fields with editor sidebar panels (weekly hours and days-off field types).
  * **Forms**: a contact form block with spam guards and limits, private Messages with an unread count, retention and privacy export/erase.
  * **Icons**: an icon set and block, and a shared editor script for server-rendered blocks.
* Email footer: the address prints on one line whatever line endings were typed.

= 0.1.0 =
* First release, from a tested, live clinic build.
* Settings: a declarative API for tabs, cards and fields, with Business, Contact and Email tabs built in.
* Business menu with a Today screen, panels and count bubbles; a toolbar item for staff.
* Staff access: roles and plain-words abilities, the matrix screen and a staff list.
* Branded emails with a switch per email, SMTP, a test email and an email log.
* Sign-in protection, a security log with IP blocking, limits on registrations and password resets, XML-RPC off.
* Site stats without cookies; a branded two-panel sign-in page; on-brand dialogs and a contact sheet.
* Design styles and colour palettes for themes that support them.
* Setup wizard with starter-content importers; automatic updates from the Gradespace update server; View details.
