Coauthored with Claude
Security looms large as we enter November. There’s been a lot of bad news: a certificate authority with woefully inadequate private keys, vulnerabilities have been discovered (and exploited) in PHP and other widely used programs, and several organizations have detected Chinese open weight models misbehaving in several different ways. New models are arriving faster and faster, and the best open-weight models are catching up to frontier model performance. But organizations that would like to achieve AI sovereignty will have to ask some difficult questions about the models they adopt. Is this an inflection point? Whether or not it is, AI has changed security permanently, and things are bound to get worse before they improve.
We are starting to see models developed with recursive self-improvement; we continue to see clones of Jev; and we’ve seen text watermarks added to output generated by Anthropic’s and OpenAI’s models. (Google has been using text watermarks for some time.) But tools for detecting the watermark are hard to find. Anthropic’s and OpenAI’s are in a very limited beta, and Google’s is only available in their enterprise package. What does a watermark mean if you can’t detect it? And, as we get more accustomed to AI-generated output, will anyone care?
There are more choices than ever for agents, along with new standards for the interaction between agents and models. In addition to general purpose agents, there are also agents specialized for coding, HR, BI, and more. And most agents will connect to many models, including local ones. Choose agents as carefully as you choose models; your agent is where you will do your work.
At last month’s SciFoo, Hugo Bowne-Anderson said that we’ve been through two major changes in the past few years: from writing code to letting AI write the code, and from reading the code to not reading the code. These changes are as radical as the shift from programming in machine language to programming in high-level languages. We will all go through a process of rethinking how we work and what we’re trying to accomplish.
Security has been much in the news. Not all of the news is bad: AI was used to discover the PHP2Shell vulnerability and to develop a fix. The leaked CVE and the many out-of-date PHP sites online are serious problems, but not problems attributable to AI. Too many problems arise from getting the basics wrong: A certificate authority using 4 as its private key is a mistake that should never have been made. As always, security is about getting the basics right while watching for new threats. And while organizations claim to be paying more attention to security, the word from insiders is that “attention” means lip service. In the long run, AI may make the world a safer place, but the next few years look scary.
There’s been a lot of news about certificate management, not all of it good. It’s time to check on your certificate providers, get ready for quantum-resistant encryption, and automate as much of the process as you can.
Mark Twain didn’t quite say, “The reports of my death are greatly exaggerated.” But we are hearing that the Web is dead, and that’s indeed an exaggeration. The flood of crawler traffic and the disincentives towards human writing are a problem. But what does “death” mean for a technology so flexible that you can build an office suite that stores entire documents’ URLs?
DataBreaches.net previously covered the 2023 Lehigh Valley Health Network breach by AlphV (aka “BlackCat”). The breach garnered a lot of public attention when the threat actors crossed a line by posting nude pictures of patients on their leak site. In 2024, patients settled civil litigation for $65 million. Now HHS has settled its own charges...
Have you ever replaced fifty lines of JavaScript with a few lines of CSS, merged the PR, and felt like you just did the codebase a favor? You replace ResizeObserver with a container query, the diff is mostly red, and everyone approves it before lunch.
This was what it looked like before you made the change:

:has(img) rule matches any descendant image, so the comment’s avatar counted as a hero image. And the p element selector matches any descendant paragraph. As a result, the comment’s text was caught in the selection.:has() adds no weight by itself, but its argument does. That gives .card:has(img) p a specificity score of 0-1-2, while .comment p comes in at just 0-1-1 and loses the battle.It could get even more complex and difficult to catch because the state changes at runtime. Let’s say a user posts a comment that includes an avatar. The body text of the card above it loses its spacing. Good luck reproducing that from a bug report that says “spacing looks off sometimes.”
The fix is to constrain the selector with child combinators:
.card:has(> img) > p {
margin: 0;
}
This helps the selector get more specific about what it is yelling about.
Native nesting has the same habit. MDN’s guide to nesting and specificity says the & selector takes “the largest specificity in the associated selector list,” exactly like :is(). Look at this adaptation of MDN’s example:
/* Specificity 1-0-0 */
.card, #featured {
/* Specificity: 1-0-1 */
& a {
color: blue;
}
}
/* Specificity: 0-1-0 */
.button {
color: red;
}
A .button link inside a .card is blue, even though the #featured ID is nowhere near it. The usual reaction is a more specific .button rule, then an exception to that rule. Before you know it, you are in a war of specificity you never knew you had declared.
The way out is to keep the selector list and drop its weight by wrapping it in :where():
/* :where() always weighs 0-0-0 */
:where(.card, #featured) {
/* Specificity: 0-0-1 */
& a {
color: blue;
}
}
/* Specificity: 0-1-0 */
.button {
color: red;
}
Plain links inside a card are still blue, and the .button link is red again. Again, another way to quiet the loud housemate.
Don’t be fooled into thinking specificity is the whole story. MDN’s introduction to the cascade puts origin, importance, and cascade layers ahead of specificity, and notes that “normal layered styles have less precedence than normal unlayered styles.” In my test, a layered rule with a specificity of 0-4-0 lost to an unlayered .title (0-1-0). So if your new component styles go into @layer components while the legacy stylesheet stays unlayered, the legacy rule wins every conflict, however carefully you wrote the new one. The exception is when the !important keyword is used, because it actually reverses the cascade order.
Miriam Suzanne’s original proposal for cascade layers describes exactly the pain layers were meant to remove: having to “be very careful with matching specificity to layer.” Layers do remove it, but only when the rules on both sides of the conflict are layered. A half-layered codebase just gives you one more ordering to keep in your head.
None of these battles started with :has() or nesting. In an r/webdev thread titled “CSS-in-JS: What’s the biggest performance drop you actually felt?”, one of the most upvoted answers isn’t about performance at all. It is about the cascade:
“Here’s a fun one: refactoring your JS can cause your CSS to break, because your CSS builds are determined by JS import orders.”
— mq2thez
This is about the “order of appearance,” which is the last step in the cascade that ends up deciding a conflict nobody knew existed. Another commenter in the subreddit adds that the injected styles can land in a different order in development and production builds: “Who doesn’t love debugging something that works locally but not in prod.”
Not everyone in the thread agrees on who is to blame. TheThirdRace replies, “It’s a coding issue, not a technology issue.”
I think both are right, and that is exactly the point. CSS-in-JS is a different technology from the native features we are looking at, so take this as a neighboring example, not as proof. But the shape of the problem is the same: the tool did what it promised, and the trouble arose from how it complied with the existing rules.
Before you add the rule:
> so the selector stops at the component’s own children.:where(), which always has a specificity of 0-0-0.“We moved it to CSS, so it’s faster now.” You have probably heard this in a code review, and maybe said it yourself. In my last article, I warned that it isn’t always the case. Let’s test it.
Moving a behavior from JavaScript to CSS changes who does the work. It doesn’t tell you how much work there is. The last housemate, the browser’s rendering engine, is the one who gets the bill. Everyone in the apartment shares it, and nobody looks at it after the move.
Take :has() as an example. Browser vendors were worried about its cost from the start. In a CSS Working Group issue about :has() restrictions, Emilio Cobos Álvarez noted that implementations wanted to ship it with limitations “in order to prevent performance footguns and complexity.” Today, MDN’s :has() page has a whole section on performance with two pieces of advice:
:has() to an element with a large subtree, “like body, :root, or *.”> or +, because otherwise “the browser might need to traverse the entire subtree of the anchor element on every DOM mutation.”I wanted numbers, so I built a list with 5,000 rows. The test toggles an .is-selected class on the same row 300 times, so the row’s selected state changes on every toggle, and forces a style and layout update each time. I ran it in Chrome 153 on a Mac in October 2026, and recorded a trace in DevTools to count how many elements each toggle restyles.
See the Pen Scenario 4: The Rewrite Nobody Measured [forked] by Shoyombo Gabriel Ayomide.
| Rule | What is in the stylesheet | Median time per toggle | Elements restyled |
|---|---|---|---|
| 1 | JavaScript toggles a class on the toolbar | Under 0.1ms | 1 |
| 2 | body:has(.row.is-selected) .toolbar |
About 1.4ms | 1 |
| 3 | .list:has(.cell > b) .row, with the toolbar handled by JavaScript |
Under 0.1ms | 1 |
| 4 | Rules 2 and 3 together | About 7ms | 5,001 |
| 5 | For scale: JavaScript restyles all 5,000 rows on every toggle | About 6ms | 5,001 |
There are two lessons in that table.
First, the “bad” selector was fine. Rule 2 is exactly the shape MDN warns against: a :has() anchored to body. It cost about a millisecond and a half more than the script, and it restyled a single element, the toolbar. A frame at 60Hz lasts 16.7ms, so I would take that trade for deleting an event handler any day.
Second, two harmless rules were not harmless together. Rule 3 styles the rows, and its :has() condition remains unchanged throughout the test. On its own, it costs nothing measurable. Put it in the same stylesheet as the second rule instead, and every toggle restyles all 5,000 rows plus the toolbar. That is about what it costs a script to restyle every row on purpose.
Let’s keep this in proportion. Since 7ms still fits inside a frame, nobody would notice it on this page. However, the browser is now restyling 5,001 elements to change just one, and neither rule says so. Also, the cost grows with the list. When I set the demo to 20,000 rows, the two rules together restyled 20,001 elements and took about 31ms per toggle, which is nearly two frames.
I can tell you what Chrome does here, but not why. Changing .is-selected on one row restyles every element that the other :has() rule targets, even though that rule never mentions .is-selected and its result never changes. I have filed this with the Chromium team as an issue, with a reduced test case, and I only tested Chrome. Whatever the cause, neither rule looks suspicious in a code review, and the cost applies only when both are met.
That is system complexity in a single table. And please take the pattern from it, not the numbers: this is one page, one browser, and one machine.
The same goes for animation. The animation guide on web.dev recommends restricting animations to opacity and transform where possible, and warns that “an animation that moves an element using something other than transform is likely to be slow.” MDN’s CSS performance guide lists the properties that trigger layout when animated, such as width, height, margin, top, and left. A CSS transition on left runs layout on every frame, while a JavaScript animation of transform skips it. The property determines the cost, regardless of which language you write it in.
To be fair, CSS often wins. A script that reads layout, writes styles, and reads layout again forces the browser to do layout several times in one frame, and a container query hands that whole job to the browser. But that win is something you can measure, so measure it:
Each of these scenarios is small on its own. The trouble is that a real migration could hit all four scenarios at once. You have already seen three of them. They are the three bugs in the “after” demo at the top of this article. Add a long list, and you get the fourth. One move-in, four housemates disturbed.
A team replaces a card’s JavaScript-driven responsive layout with a container query and :has(). The pull request adds twelve lines of CSS, deletes one script, and misses another. Within a month, three bug reports land, and there’s a chance a performance trace turns up as the fourth problem:
.card--wide, and one rule still reads it. The developer goes digging through the JavaScript.0px. The developer reads the parent’s layout..card:has(img) p matches. The developer traces the winning declaration in DevTools.:has() rules cost about 7ms per selection. That fits within a frame, even though each selection restyles 5,001 elements, whereas it used to restyle one. As the number of rows increases, the elements restyled increase, and the cost of the two :has() rules per selection will also increase, but who renders a table with a large number of rows without pagination?None of these reports means that container queries or :has() were the wrong choice. Each bug lives in the meeting point between the new CSS and the system it moved into. Still, one small pull request has sent a developer through the JavaScript, the page layout, the cascade, and the profiler.
And the way out is not a return to JavaScript. Here is the same page once more, fixed.
See the Pen demo (fixed PR) [forked] by Shoyombo Gabriel Ayomide.
It still uses a container query and :has(), with three changes:
.card--wide rule are removed, so the layout has a single owner.This gap between a clean start and a real codebase shows up in the same r/webdev thread. The person who started it, JRM_Insights, opens with an observation that could describe any of our four scenarios:
“I often find that the initial tooling simplicity gives way to harder-to-debug runtime styling issues.”
— JRM_Insights
Further down, mq2thez describes moving an old CSS-in-JS codebase to a newer pattern: “In isolation or greenfield maybe it wouldn’t be a huge issue,” but in practice, it meant rewriting “whole component trees.” TheThirdRace, who had been defending the technology, answers that they had only ever evaluated it “from a greenfield project perspective,” and that it was “very enlightening to see this through the lens of a spaghetti CSS migration.”
That is the honest version of this whole article.
Most of what we read about a new feature is written from the greenfield perspective. Most of what we ship is a migration.
Your Migration ChecklistBefore you replace JavaScript with a modern CSS feature, or adopt one in an existing codebase, answer these six questions. Think of them as the conversation to have with the housemates before the new tenant gets a key. I suggest putting the answers right in the pull request description.
Modern CSS is great and a huge improvement to styling as a whole. In particular, container queries and :has() let us express things that used to need a script, a dependency, or a hack. But a new capability does not hand us a simpler system by default. In every scenario we looked at, the new tenant did something that didn’t sit well with others, and that was because nobody introduced them to the housemates’ ways of doing things. We get a simpler system when the new rule has one clear owner, a scope we have checked, a place in the cascade we chose on purpose, and numbers showing it helped.
So, the next time a pull request deletes more than it adds, enjoy the red lines. Then ask where the complexity went, and run the six questions. If you can’t answer one of them, the migration isn’t finished yet.
Referencescontainer-type (MDN):has() (MDN)
I decided not to do #NaNoWriMo this year. Part of that is because I’ve never done it. Never even attempted it. Don’t even want to. Writing a novel is a life goal of mine, but cramming it all into one month isn’t for me. November is impossibly busy. But I do think it would be cool to have a big excuse for avoiding all those obligations.
“Dave, come to this holiday party!”
“Sorry, I’m writing a novel.”
“Dave, come work at the school’s fall carnival!”
“Sorry, I’m writing a novel.”
“Dave, pay attention to your kids!”
“Sorry, I’m writing a novel.”
See, that level of self-preservation and pre-emptive boxing out of other responsibilities would be so cool. Like cigarettes cool.
Despite not having a novel excuse, I am feeling compelled to wri’ mo’ this month. Whether it’s autumnal ghosts or pumpkin spices, there’s a draw pulling me back to my text editor. A vapor of creativity stolen by corporate dementors and their infernal machines, seeping back into my lifeless shell.
Last week I powered through a handful of short stories by Arthur C. Clarke. Vintage sci-fi inspires me. Partly because the experimental short form factor containing a single “What If?” thought experiment seems achievable and because most of the stories are complete shit. In a modern world of blockbuster movie adaptations and eight volume octologies, vintage sci-fi sets the bar real low.
One story I came across was The Nine Billion Names of God. The audiobook has a sick-ass cover of a skeleton in a cracked space helmet on a dying planet with a giant sun rising in the distance. You can pause this and get through it in ten minutes.
Nothing in the story relates to that cover. Rather, the story is about two Western men that work for a fancy computer company that makes the Mark V. They’re hired by a group of Tibetan monks to help them write a program that will procedurally generate the nine billion names of God which they believe will bring about the end of the world. The Mark V will shorten the task of writing down all the possible names of God from 15,000 years of manual writing down to 100 days! Incredible productivity gains!
AAABBBAAA AAABBBABA AAABBBACA AAABBBADA AAABBBAEA AAABBBAFA AAABBBAGA AAABBBAHA AAABBBAIA AAABBBAJA
AAABBBAAB AAABBBABB AAABBBACB AAABBBADB AAABBBAEB AAABBBAFB AAABBBAGB AAABBBAHB AAABBBAIB AAABBBAJB
AAABBBAAC AAABBBABC AAABBBACC AAABBBADC AAABBBAEC AAABBBAFC AAABBBAGC AAABBBAHC AAABBBAIC AAABBBAJC
AAABBBAAD AAABBBABD AAABBBACD AAABBBADD AAABBBAED AAABBBAFD AAABBBAGD AAABBBAHD AAABBBAID AAABBBAJD
AAABBBAAE AAABBBABE AAABBBACE AAABBBADE AAABBBAEE AAABBBAFE AAABBBAGE AAABBBAHE AAABBBAIE AAABBBAJE
AAABBBAAF AAABBBABF AAABBBACF AAABBBADF AAABBBAEF AAABBBAFF AAABBBAGF AAABBBAHF AAABBBAIF AAABBBAJF
AAABBBAAG AAABBBABG AAABBBACG AAABBBADG AAABBBAEG AAABBBAFG AAABBBAGG AAABBBAHG AAABBBAIG AAABBBAJG
... ... ... ... ... ... ... ... ... ...
THIS STORY HAS NO MODERN DAY PARALLELS. An altruistic sex cult from a specific locale using a mountain of money to hasten the end of the world with their enormous computers that endlessly generate combinations of nonsense words that they believe will summon God… well, that would be a work of cheesy science fiction, not real life.
The Nine Billion Names of God flipped a switch in me and has renewed my courage to write more. As someone with ideas about ideas burning holes in my head and it’d be worthwhile to offload some of my cognitive surplus. I don’t have any solid plans, words-per-day goals, charts, graphs, storyboards, outlines, or some Benjamin Franklin life hack schedule to make this happen. Instead I have an Obsidian vault with a hundred unsorted drafts.
I don’t want commit myself to publishing every day, but my goal is to get up and spend thirty minutes to an hour each day writing something. I want to carve out time before work, before doing my puzzles, before doom-scrolling the news. I want to use those premium dopamine reserves –the flavorful floating creme of consciousness that comes from a good night’s sleep, not the metallic bitter dregs distilled from caffeine and stimulants– I want to savor and use that precious natural dopamine on writing.
My promise to you, dear reader, is that I will write more this month. I will write no less than nine billion stories and/or blog posts by the end of November. And upon completing my works, I can shed this mortal coil and leave this forsaken world.
1234. This week, we look at how small writing errors in proposed laws have cost millions of dollars and knocked whole measures off the ballot. Then, we look at the nautical origin of "batten down the hatches."
A hearty thank-you to our Keepers of the Commas and one mysterious Guardian of the Grammary on Patreon. We appreciate your support!
🔗 Share your familect recording in Speakpipe or by leaving a voicemail at 833-214-GIRL (833-214-4475)
🔗 Watch my LinkedIn Learning writing courses.
🔗 Subscribe to the newsletter.
🔗 Find an edited transcript.
🔗 Get Grammar Girl books.
| HOST: Mignon Fogarty
| Grammar Girl is part of the Quick and Dirty Tips podcast network.
| Theme music by Catherine Rannus.
| Grammar Girl Social Media: YouTube. TikTok. Facebook. Threads. Instagram. LinkedIn. Mastodon. Bluesky.
Hosted on Acast. See acast.com/privacy for more information.