=== Theme SCSS Compiler ===
Contributors:      simonmista
Tags:              scss, sass, css, compiler, theme
Requires at least: 6.3
Tested up to:      7.1
Stable tag:        1.0.8
Requires PHP:      8.1
License:           GPLv2 or later
License URI:       https://www.gnu.org/licenses/gpl-2.0.html

Compile SCSS to CSS in the WordPress admin. No Node.js, no npm, no build step. Several file pairs, per-file cache busting, rebuild on change.

== Description ==

Your theme has a `.scss` file. Your site needs a `.css` file. Theme SCSS Compiler does that one conversion, on your server, from Tools → Theme SCSS Compiler.

You give it a source and a target, for example `assets/scss/style.scss` and `assets/css/style.css`. Save the SCSS, open any admin page, and the CSS is rebuilt. No Node.js, no `npm install`, no Gulp task, no CI step: the Sass compiler (scssphp) ships inside the plugin. SCSS is the Sass syntax that looks like CSS. Every valid CSS file is already valid SCSS, and you get nesting, variables, mixins and `@import` on top.

It is built for the case that keeps coming up: a theme with one or two stylesheets, hosting where a build pipeline is not going to happen, and someone who wants to change a variable and see the result.

= Features =

* As many SCSS to CSS pairs as the theme needs. Both paths are relative to the active theme.
* A Frontend or Admin context per pair, enqueued on the matching hook.
* A cache-busting version per pair, applied to the stylesheet URL through the `style_loader_src` filter.
* Versions move only when that pair's compiled CSS really came out different.
* Import tracking. Edit a partial and the next admin page load rebuilds.
* Compressed output for production, expanded when you need to read it.
* Auto-compile on missing or stale CSS, so a deploy does not leave the site unstyled.
* An optional recompile timer on the settings page. Off by default.
* Auto-enqueue that steps aside when a stylesheet URL is already registered.
* A Theme CSS status table listing the stylesheets it finds in the active theme.
* Compile button with live feedback and a persistent error panel.
* Compiled CSS is swapped into place atomically, so a visitor is never served a half-written stylesheet.
* A compile lock, so two administrators pressing Compile at the same moment do not build the same CSS twice.
* Configuration from PHP constants instead of the database, for `wp-config.php`, a theme, or Bedrock.
* Administrators only by default, with a capability filter.
* PHP 8.1+, accessible admin UI, German translation included.

= Scope =

The whole plugin works inside one theme, the active one. Both paths of a pair resolve against it, and that is also the write boundary: a path containing `..` is refused, and nothing is ever written outside the theme. An `@import` inside your SCSS can still read a file elsewhere on the server, and output built from one is refused before it reaches disk. With a child theme active, the child is that theme.

That keeps the tool small on purpose. One theme, its stylesheets, nothing else to configure.

= Where the compiled CSS goes =

Compiling writes into your theme. It writes the CSS target of each pair, plus a short-lived temporary file beside it that is renamed onto the target when the compile finishes, and it creates the directory for the target if it is missing. Nothing else in the theme is touched: sources are only read, and a target that is not a `.css` file is refused before the write, so a mistyped path cannot land on `functions.php`.

Commit those CSS files or add them to `.gitignore`. Both work, because auto-compile rebuilds anything missing.

= Privacy =

No external HTTP requests, no cookies, no telemetry, no tracking. Everything happens on your server. The plugin bundles scssphp and its dependencies (league/uri, scssphp/source-span, symfony/filesystem, the symfony ctype and mbstring polyfills, and the PSR HTTP interfaces). All are MIT licensed and GPL-compatible, and their source sits in the plugin's `vendor/` directory.

== Installation ==

1. Upload the `theme-scss-compiler` folder to `/wp-content/plugins/`, or install the ZIP through **Plugins → Add New**.
2. Activate it.
3. Open **Tools → Theme SCSS Compiler** and set the SCSS source, the CSS target and the context for each pair.
4. Click **Save settings**. Compiling builds the saved configuration, so **Compile now** stays locked while there are unsaved changes.
5. Click **Compile now**. After that, saving a SCSS file and loading any admin page is enough.

If your theme has no `assets/scss/style.scss`, replace the default pair in step 3 with your own paths. Auto-compile skips a pair whose source does not exist; **Compile now** reports the missing source.

Configuration can also live in code. See the FAQ entries on `TSCSSCOMPILER_PAIRS`.

== Frequently Asked Questions ==

= Do I need Node.js, npm or a build step? =

No. The compiler is PHP and ships with the plugin. Nothing is installed on the server, nothing runs on your machine. You need WordPress 6.3 and PHP 8.1 or newer.

= Which Sass features work? =

Everything you reach for in a theme stylesheet: nesting, the `&` parent selector, variables, mixins, functions, `@extend`, `@media` nesting, arithmetic, interpolation, and `@import` for splitting your styles into partials. Import chains nest as deep as you like, and every file in the chain is tracked for change detection.

Partials are the `@import` kind, so `_variables.scss` is pulled in with `@import "variables";` at the top of your entry file, and everything it declares is available below. The compiler is a PHP port of Sass rather than Dart Sass, so it is worth a look at the compiled output the first time you move an existing theme over.

= How do I split my styles into partials and share variables? =

Give a partial a leading underscore and pull it in with `@import` at the top of your entry file. Everything a partial declares is available to everything below it.

    // assets/scss/_variables.scss
    $brand:  #0073aa;
    $text:   #1f232b;
    $radius: 8px;

    // assets/scss/_card.scss
    .card {
        color: $text;
        border-radius: $radius;
    }

    // assets/scss/_menu.scss
    .menu a {
        color: $brand;
    }

    // assets/scss/style.scss — the file you set as the pair's SCSS source
    @import "variables";
    @import "card";
    @import "menu";

Note the import name drops both the underscore and the extension: the file `_card.scss` is imported as `"card"`.

Two things follow from this, and both are easier to get right if you know them up front.

**Order matters.** A partial is read at the position where you import it, so `@import "variables"` has to come before the partials that use those variables. Variables first, the rest below.

**One shared scope.** Every partial writes into the same namespace, so two partials that each declare `$padding` will quietly disagree — last one wins. Prefix instead: `$card-padding`, `$menu-padding`. Same for mixin names.

When the list gets long, collect it in one index partial and import just that:

    // assets/scss/_index.scss
    @import "variables";
    @import "card";
    @import "menu";

    // assets/scss/style.scss
    @import "index";

Chains nest as deep as you like. The plugin records every file that was read at any depth, so editing `_card.scss` two levels down still triggers the rebuild on your next admin page load.

= I am new to SCSS. What does it actually look like? =

Every valid CSS file is already valid SCSS, so you can rename `style.css` to `style.scss` and start there.

**Nesting.** Write child selectors inside the parent instead of repeating it:

    .card {
        padding: 1rem;
        a { color: rebeccapurple; }
    }

compiles to `.card { padding: 1rem; }` and `.card a { color: rebeccapurple; }`.

**The `&` parent reference,** useful for states:

    .button {
        background: #0073aa;
        &:hover  { background: #005177; }
        &.active { background: #003f66; }
    }

compiles to `.button`, `.button:hover` and `.button.active` with those three colours.

**Variables.** Set a value once and reuse it:

    $brand: #0073aa;

    a       { color: $brand; }
    .button { background: $brand; }

One thing that surprises people: the default output style is compressed, so the compiled file is one long line and `rebeccapurple` has become `#639`. Set the compiler to **Expanded** while you are learning. Full language guide: https://sass-lang.com/guide/

= Does the plugin change files in my theme? =

Yes, one kind of file. It is a compiler, so the boundary matters more than a reassuring "no".

It writes the CSS target of each pair, for example `assets/css/style.css`. That file is replaced in full on every compile, so treat it as build output and never hand-edit it. It also creates the directory for that file when the directory does not exist.

Each compile briefly writes a second file next to the target, `style.css.tscsscompiler-tmp`, and renames it onto the target when it is finished — that is what makes the swap atomic. It is gone by the time the compile returns. The name deliberately does not end in `.css`, so if a process is killed part-way the leftover is never served as a stylesheet; you can delete it.

It writes nothing else. Your SCSS is only read. `functions.php`, templates and every other non-`.css` file are unreachable, because a target without a `.css` extension is refused before the write. Nothing outside the active theme can be written either: `..` is rejected, and a target that turns out to be a symlink pointing out of the theme is rejected too.

Cache-busting versions are never written into a file. They go onto the stylesheet URL at request time through the `style_loader_src` filter, so your theme's own `wp_enqueue_style()` calls stay as they are.

One thing to watch: the target does not have to be a new file. Point a pair at a `.css` file that already exists in your theme and that file is replaced.

= Can I compile into my theme's own style.css? =

No, that one target is refused. `style.css` is where WordPress reads `Theme Name:` from, and a compile replaces the whole file, so one typo there would leave the theme unrecognisable and drop it out of **Appearance → Themes**. The plugin says so instead of writing.

Compile to a separate file and enqueue it alongside your `style.css`:

    assets/scss/style.scss  →  assets/css/style.css

Any other name works too. Only the theme's own root `style.css` is off limits; a `style.css` in a subfolder is fine.

= When does the cache-busting version change? =

Only when that pair's freshly compiled CSS differs from the file already on disk. Other pairs keep their version.

An edit that produces identical CSS does not move the version. That covers `//` comments in any mode, and `/* */` comments in compressed output, because those are stripped. In expanded output a `/* */` comment is part of the CSS, so editing one is a real change and does bump the version.

Versions are three-part numbers and the patch digit advances: `1.0.4` becomes `1.0.5`.

= What is the difference between Auto-compile and the auto-recompile timer? =

Two separate settings, with different defaults.

**Auto-compile** is on by default. On every wp-admin page load it checks whether any compiled CSS is missing or older than its SCSS source or tracked partials, and rebuilds what needs it. Nothing runs on a schedule; your own page load triggers it.

**Auto-recompile** is off by default. Turn it on and the settings page runs a countdown, from 1 to 999 seconds, and re-checks on each tick while that page stays open in your browser. It counts down only while that tab is the one you are looking at, and it holds off while the form has unsaved changes; the readout says which of the two is happening. It compiles only when something actually changed, so a page left open does not keep rewriting the CSS.

They are independent. Switching Auto-compile off does not stop the timer. To compile only on the button, switch both off.

= My CSS is not updating. What do I check? =

In this order:

1. Did you save the settings? **Compile now** is locked while the form has unsaved changes, and clicking it then does nothing.
2. Press **Compile now** and read the message. A missing source file or a syntax error shows up here.
3. Look at the pair's version. If it did not move, the compiled CSS came out identical and the browser is correctly serving what it has.
4. Check whether "Bump CSS version after each compile" is on. With it off the URL never changes and browser caches keep the old file.
5. If pairs come from `TSCSSCOMPILER_PAIRS`, versions are never rewritten. Edit the `version` value in your code.
6. Check your caching plugin and CDN. A version in the URL does not help if something upstream serves a cached response.

= Where do the compiled CSS files go, and what if they are missing after a deploy? =

Each target is written inside the active theme at the path you configured. With Auto-compile on, the next wp-admin page load notices a missing file and builds it, so a deploy or a `git pull` without compiled CSS does not leave the site unstyled. Nothing rebuilds until someone with access opens an admin page, though, and the pair is not enqueued in the meantime. Deploying to production without visiting wp-admin means you should commit the compiled CSS.

= Does it detect changes in `@import`-ed partials? =

Yes. After each successful compile the plugin stores every file the compiler read, including nested import chains, and Auto-compile compares each of them against the compiled CSS. So if `style.scss` does `@import "menu";` and you only edit `_menu.scss`, the next admin page load recompiles. You never have to touch the entry file.

= Does anything compile on the front end, for visitors? =

No. Compiling happens only in wp-admin, for a logged-in user who passes the capability check, and it is triggered in exactly three places: an admin page load with Auto-compile on, the **Compile now** button, and the settings-page timer. The last two are admin-ajax requests, so also wp-admin. There is no WP-Cron job and no scheduled task. Visitors are served the already compiled file and never cause any Sass work.

= Where do I see compilation errors? =

On **Tools → Theme SCSS Compiler**. A failed compile is kept in a "Last compile error" panel with server paths stripped from the message, and a successful compile clears it. The stored error also expires by itself after 24 hours. **Compile now** reports success or failure right away. There is no notice on other admin screens, so a failed auto-compile is visible only on the plugin's own page.

= Frontend or Admin: what does the Context setting do? =

Each pair has one. **Frontend** pairs are enqueued on `wp_enqueue_scripts`, so on the public site only. **Admin** pairs are enqueued on `admin_enqueue_scripts`, so in wp-admin only. The version filter applies in both. New pairs start as Frontend.

= Does it work with child themes? =

Yes, and the active child theme is the boundary. Paths resolve against the active stylesheet directory, which is the child when one is active. That is also the limit: a path containing `..` is refused, and while an `@import` can read a file in the parent theme, the compile is stopped before anything is written. If the SCSS you want to compile lives in the parent theme, configure it while the parent is active, or copy the sources into the child.

= Can the CSS target sit outside the theme? =

No. Both paths resolve against the active theme, a path containing `..` is rejected, the target must end in `.css`, and a symlinked target pointing out of the theme is refused. Writes cannot leave the theme.

Reads are a different matter. An `@import` in your SCSS can name a file anywhere the PHP process can read. That file is read and parsed, and only then does the plugin refuse to write output built from it. The contents never reach a public CSS file, but anyone who can edit the SCSS can make the compiler open a file elsewhere on the server. Treat access to this plugin the way you treat access to the theme editor.

= What happens if I deactivate or delete the plugin? =

Deactivating stops the enqueuing. If the plugin was enqueueing your stylesheets, they are gone from the page until you reactivate it or add `wp_enqueue_style()` to your theme. The compiled CSS files stay on disk.

Deleting removes the plugin's options and transients, on every site of a multisite network. It does not delete the compiled CSS in your theme. Those files are yours and remain.

= Should the plugin enqueue my CSS, or should I do it in `functions.php`? =

Either. Auto-enqueue is on by default: the plugin calls `wp_enqueue_style()` for each pair on that pair's context, as late as the hook allows, after everything else has registered, and skips a file whose URL is already registered so it does not load twice. That check is a URL match at the moment the hook runs, so a stylesheet registered later, or registered under a differently written URL such as `http://` against `https://`, can slip past it.

Enqueueing in `functions.php` is more predictable, and it means the front end does not depend on this plugin staying active. Switch Auto-enqueue off and enqueue the compiled file yourself. The version filter keeps working either way.

= Who can use the plugin, and what does that permission allow? =

Administrators only, through the `manage_options` capability. The menu entry, the save handler, both AJAX endpoints and the auto-compile hook all check it, so Editors and Authors neither see nor reach it. To open it up, filter the capability:

    add_filter( 'tscsscompiler_capability', static function () {
        return 'edit_theme_options';
    } );

Do that carefully. Whoever passes the check can set the file pairs, write CSS into the active theme, publish it on the front end, and make the compiler read any file the web server can read. It is a code-adjacent permission, so keep it with roles you already trust with the theme.

= Can I configure everything in code instead of the admin form? =

Yes, one setting at a time. Define the constant for the setting you care about and it wins over whatever the database holds. You do not have to move the rest of your configuration into code with it.

    // Enough on its own. The database keeps every other setting.
    define( 'TSCSSCOMPILER_OUTPUT_STYLE', 'expanded' );

A setting a constant supplies is shown on the settings page but its control is disabled, and the page names the constants in play so you know where to look. That is deliberate: an editable control would let you store a value that the constant overrides on the very next page load.

Here is the full set. Where a constant is absent, the value after the arrow is the fallback the database starts from.

* `TSCSSCOMPILER_AUTO_COMPILE`, `true` / `false` → `true`
* `TSCSSCOMPILER_AUTO_ENQUEUE`, `true` / `false` → `true`
* `TSCSSCOMPILER_OUTPUT_STYLE`, `'compressed'` / `'expanded'` → `'compressed'`
* `TSCSSCOMPILER_AUTO_RECOMPILE`, `true` / `false` → `false`
* `TSCSSCOMPILER_AUTO_COMPILE_INTERVAL`, seconds from 1 to 999 → `30`
* `TSCSSCOMPILER_BUMP_VERSION`, `true` / `false` → `true`

The file pairs work the same way, with one extra consequence:

    define( 'TSCSSCOMPILER_PAIRS', [
        [ 'scss_path' => 'assets/scss/style.scss',       'css_path' => 'assets/css/style.css',       'version' => '1.0.0', 'context' => 'frontend' ],
        [ 'scss_path' => 'assets/scss/style-admin.scss', 'css_path' => 'assets/css/style-admin.css', 'version' => '1.0.0', 'context' => 'admin'    ],
    ] );

All four keys are expected: `scss_path`, `css_path`, `version` (three-part, or it falls back to `1.0.0`) and `context` (`frontend` or `admin`, anything else becomes `frontend`).

The consequence: with the pairs in code the version numbers are in code too, so nothing can bump them. Version bumping is switched off and locked while `TSCSSCOMPILER_PAIRS` is defined, whatever `TSCSSCOMPILER_BUMP_VERSION` says. Edit the `version` value yourself when you want the URL to change.

Whatever the constants override stays untouched in the database, so removing a `define()` again gives you back the value you had before.

= Where do I put the `define()` calls? =

**`wp-config.php`** is the simplest place, and it loads before any plugin:

    define( 'TSCSSCOMPILER_PAIRS', [
        [ 'scss_path' => 'assets/scss/style.scss', 'css_path' => 'assets/css/style.css', 'version' => '1.0.0', 'context' => 'frontend' ],
    ] );
    define( 'TSCSSCOMPILER_OUTPUT_STYLE', 'compressed' );

**Your theme's `functions.php`** works as well. Hook `after_setup_theme`; the plugin does not read its settings before then:

    add_action( 'after_setup_theme', static function () {
        if ( ! defined( 'TSCSSCOMPILER_PAIRS' ) ) {
            define( 'TSCSSCOMPILER_PAIRS', [
                [ 'scss_path' => 'assets/scss/style.scss', 'css_path' => 'assets/css/style.css', 'version' => '1.0.0', 'context' => 'frontend' ],
            ] );
        }
    } );

**Bedrock and `.env`.** The scalar options bridge straight through, which is the usual reason to reach for this: expanded output on staging, compressed in production, one line per environment.

    # .env
    TSCSSCOMPILER_OUTPUT_STYLE=expanded

    # config/application.php
    if ( $style = env( 'TSCSSCOMPILER_OUTPUT_STYLE' ) ) {
        define( 'TSCSSCOMPILER_OUTPUT_STYLE', $style );
    }

Pairs are a nested array and do not fit in `.env`. If you want those from code too, put the `define( 'TSCSSCOMPILER_PAIRS', [ … ] )` in `config/application.php` directly.

= Why is one of the settings greyed out? =

Because a `TSCSSCOMPILER_*` constant supplies it, from `wp-config.php`, your theme, or a Bedrock bridge. Only that setting locks; everything you did not define a constant for stays editable and saves normally. The notice at the top of the page lists the constants it found, so you know what to go looking for.

Saving is refused on the server for a locked setting, not merely disabled in the browser, and the value stored in the database is left as it was. Define `TSCSSCOMPILER_PAIRS` and the file-pair list locks the same way.

= Does it work on multisite? =

Yes, per site. Settings and the tracked partials are per-site options and the page is a per-site **Tools** submenu, so a network-activated plugin has to be configured on each site separately. There is no network admin screen. Deleting the plugin cleans up on every site of the network.

= What does the Theme CSS status table show me? =

Every `.css` file it finds in the active theme, annotated with what is known about it: whether it belongs to a compiler pair, with that pair's version and context; whether it happens to be registered in the current request; or whether it is simply a stylesheet that lives in the theme. The theme's own `style.css` shows the version from its theme header; the other files have none to show. It answers "what else is loading CSS in this theme".

It scans the active theme only, so with a child theme active the parent's stylesheets do not appear. It skips `vendor`, `node_modules`, `bower_components` and hidden folders, and stops at 250 files with a notice.

= Is the admin UI accessible? =

The settings page is built and measured against WCAG 2.1 AA.

Every field has a label tied to it, including the rows you add with the **Add file pair** button. Each row is a named group ("File pair 2"), so a screen reader tells you which pair a field belongs to instead of reading four identically-named fields over and over, and the numbering is rewritten when you add or remove a row. Removing a pair moves focus to a neighbouring control rather than dropping it on the page body.

Compile feedback goes through `role="status"` and `role="alert"` regions and is announced once, politely. The countdown itself is deliberately not a live region, so it does not read out every second, and it has a **Pause auto-recompile** button so you can stop it. If a file pair is rejected on save, the page says so in text and explains why, rather than reporting a plain "Settings saved."

Contrast is measured, not estimated: text is at 4.5:1 or better and every control boundary and focus indicator at 3:1 or better. Focus is drawn with a real outline instead of a shadow, so it survives Windows High Contrast mode, where the custom checkbox and selects hand rendering back to the browser. Decorative icons are `aria-hidden`, headings run without skipping a level, and the status table scrolls inside its own box so the page never scrolls sideways.

Not yet done, for honesty: input that was rejected on save is not handed back to you for correction, so WCAG 2.2's Redundant Entry is still open. If you hit an accessibility problem, please report it.

= Is it translated into German? =

Yes, German ships with the plugin and the catalogue is complete: every admin string, hint and error message. It loads on every supported WordPress version. Worth knowing if you maintain something similar: WordPress only started registering a plugin's own `languages/` directory by itself in 6.8. On 6.3 through 6.7 the plugin registers that directory with WordPress as it loads, and the catalogue is then read on demand at the first translated string. A wordpress.org language pack still takes precedence.

== Screenshots ==

1. The settings page under Tools. File pairs with a version and a Frontend/Admin context each, separate cards for Auto-compile and the compiler settings with their own Save buttons, and the Theme CSS status table listing the stylesheets found in the active theme.
2. Auto-recompile switched on. While the settings page is open, a countdown re-checks the sources and rebuilds only what actually changed, and the button underneath pauses it. Off by default.

== Changelog ==

= 1.0.8 =
* Compatibility: tested with WordPress 7.1.
* Changed: every `TSCSSCOMPILER_*` constant now works on its own and locks the setting it supplies. Until now all of them except `TSCSSCOMPILER_PAIRS` were ignored unless that one was defined as well. `TSCSSCOMPILER_PAIRS` also switches version bumping off and locks it, because the versions live in the constant.
* Changed: a setting that a constant supplies keeps its stored database value when you save.
* Fixed: the bundled German translation now loads on every supported WordPress version, and file sizes follow the site language.
* Fixed: compiled CSS is written beside the target and renamed onto it, so a visitor is never served a half-written stylesheet. The file keeps its permissions, and the compile lock is no longer released by a request that does not hold it.
* Fixed: the "Settings saved" confirmation is no longer shared between administrators — one admin's message could be shown to, and swallowed by, another. A screen reader now announces it.
* Security: the theme's own style.css is refused as a CSS target — WordPress reads the theme header from it, and a compile would overwrite it. An array submitted where a path belongs no longer raises a PHP warning.
* Accessibility: a WCAG 2.1 AA pass over the settings page. Labels on file pairs added in the browser, named pair groups, a pause button for the countdown, contrast on borders, focus rings and text, a reflow fix for the stylesheet table, plus forced-colors and right-to-left support.
* Documentation: Description and FAQ rewritten.

= 1.0.7 =
* Added: optional auto-recompile timer that runs only on the plugin's own settings page (Tools → Theme SCSS Compiler) – while that page is open it rebuilds on an interval you set (default 30 seconds), and only when a source actually changed. Off by default.
* Changed: the settings page is reorganised – Auto-compile and Compiler settings each get their own card and Save button, and Compile now sits with the file pairs.
* Changed: Compile now is locked while there are unsaved changes, so it always builds the saved configuration – save first, then compile.
* Fixed: change detection refreshes file timestamps on each run, so an edit is not missed when the host keeps a warm stat cache.

= 1.0.6 =
* Added: "Theme CSS status" now lists every stylesheet in the active theme, frontend and admin – each marked as a compiler pair, enqueued, or a theme file.
* Security: compiling an `@import` that resolves outside the active theme is now blocked – the CSS target must be a `.css` file, and server paths are stripped from error messages.
* Fixed: lock contention during a concurrent compile is no longer reported as a "last compile error" – the CSS was compiled correctly.
* Fixed: two pairs whose CSS files share a filename in different folders now both enqueue – the per-file version filter also keeps any existing query arguments.
* Fixed: constant-locked config no longer removes rows or reports an unsaved version bump – uninstall now clears options on every site of a multisite network.
* Accessibility: removing a file pair moves focus to a neighbouring control – it no longer drops to the page body.

= 1.0.5 =
* Fixed: "Compile now" could trigger a fatal FTP error ("ftp_nlist(): ... null given") on servers where WordPress falls back to the FTP/SSH filesystem without stored credentials. This is common on nginx/php-fpm where the web server user does not own the theme files. The plugin now writes compiled CSS directly, and an unwritable target shows a clear error instead of crashing.

= 1.0.4 =
* Fixed: fatal error on PHP 8.1 that stopped the plugin from loading. A bundled library now ships in its PHP 8.1-compatible version.
* Added: a "Settings" link in the plugin's row on the Plugins screen.
* Fixed: settings-page input fields rendered at uneven heights on WordPress 7.

= 1.0.3 =
* Compatibility: tested with WordPress 7.0. No code or behaviour changes.

= 1.0.2 =
* Fixed: "Compile now" now updates each changed pair's Version field instantly, without a page reload.
* Documentation: expanded FAQ (beginner SCSS primer, front-end behaviour, error display, git/deploy, theme-relative paths, child themes); tempered the scssphp / Dart Sass wording.
* Synced bundled libraries to the locked versions (no behaviour change).
* No configuration, options or public API changes.

= 1.0.1 =
* Fixed: editing an `@import`ed partial did not always trigger auto-recompile. When the SCSS compiler reported an included file via a non-canonical path (containing `..`, `.` or doubled slashes), the recorded dependency was silently discarded and changes to that partial went undetected. Included paths are now collapsed before being stored.
* Fixed: on installs where the theme directory is a symlink (e.g. Bedrock-style layouts), dependency tracking failed entirely – the resolved (realpath) source paths never matched the unresolved theme directory, so every dependency was dropped. The theme directory is now resolved consistently for all comparisons.
* Note: both issues affected automatic compilation only. The manual "Compile now" button was never affected, as it compiles unconditionally without consulting the dependency cache.
* No changes to configuration, options or public API.

= 1.0.0 =
* Initial release.
* Multiple SCSS → CSS pairs with per-pair version and Frontend/Admin context.
* `@import`-aware dependency tracking – every imported partial is recorded; editing a partial alone triggers auto-recompile.
* Smart change detection via content comparison – versions only bump on real CSS changes.
* Auto-compile on missing or stale output.
* Auto-enqueue with duplicate detection (runs at `PHP_INT_MAX` priority).
* Manual compile button with AJAX feedback.
* Concurrent-compile lock to prevent race conditions on busy multi-admin sites.
* Code-first configuration via `TSCSSCOMPILER_*` constants.
* `tscsscompiler_capability` filter for granting access to custom roles.
* Filesystem writes via WordPress `WP_Filesystem` API.
* WCAG 2.1 AA compliant admin UI.
* German translation included.
* PHP 8.1+ required.
* Bundles scssphp 2.1 (MIT) and dependencies.

== Upgrade Notice ==

= 1.0.8 =
Constants now work one at a time: a lone define() finally takes effect and locks just that setting, so check any you already have. The bundled German translation now loads everywhere. Atomic CSS writes, an accessibility pass, tested with WordPress 7.1.

= 1.0.7 =
Adds an optional auto-recompile timer on the settings page (off by default), reorganises the settings into clearer cards, and locks Compile now until your changes are saved.

= 1.0.6 =
Lists frontend and admin theme stylesheets in the status panel, hardens SCSS @import handling, and fixes a false compile-error notice – recommended for everyone.

= 1.0.5 =
Fixes a fatal error when compiling on servers that fall back to the FTP/SSH filesystem (often nginx/php-fpm). Recommended for everyone; required if "Compile now" crashed with an ftp_nlist error.

= 1.0.4 =
Required on PHP 8.1: fixes a fatal error that stopped the plugin from loading. PHP 8.2+ is unaffected. Also adds a Settings link on the Plugins screen and fixes input alignment on WordPress 7.

= 1.0.3 =
Compatibility update: tested with WordPress 7.0. No functional changes.

= 1.0.2 =
"Compile now" refreshes changed Version fields without a reload, plus expanded docs.

= 1.0.1 =
Bug-fix release. Recommended for everyone using auto-compile, and required if your theme directory is a symlink (e.g. Bedrock) – without this fix auto-recompile of changed `@import` partials does not work in that setup.

= 1.0.0 =
Initial release.
