/* ==UserStyle== @name DenseGPT @namespace github.com/yunmin311/DenseGPT @version 2.5.1 @description Verified compact reading layout for ChatGPT, two neutral tokens, four width presets plus a free width slider. No density algorithm, no theme detection, no page background or text colour changes. @author yunmin311 @license MIT @homepageURL https://github.com/yunmin311/DenseGPT @supportURL https://github.com/yunmin311/DenseGPT/issues @updateURL https://raw.githubusercontent.com/yunmin311/DenseGPT/main/densegpt.user.css @preprocessor default @var select dg-width-preset "Width preset" { "reading:Reading - long prose, narrowest column": "44rem", "balanced:Balanced - everyday reading*": "54rem", "wide:Wide - code blocks and tables": "66rem", "ultra:Ultra - large screens, reference material": "78rem", "custom:Custom - set it with the slider below": "initial" } @var range dg-width-custom "Custom width (used when preset is Custom)" [54, 36, 90, 1, "rem"] ==/UserStyle== */ @-moz-document domain("chatgpt.com") { /* ===================================================================== DenseGPT Scope for everything except content width: [data-message-author-role="assistant"] .markdown Nothing here reads or writes page background, body text colour or color-scheme. Light and dark are inherited from ChatGPT as shipped. ===================================================================== */ /* ======================== 0. neutral tokens ======================= Two tokens, both plain currentColor at low alpha, for the two things that should stay neutral: the inline code fill and the horizontal rule. Lightness comes from the page's own text colour, so light and dark adapt with no theme detection. The blockquote does NOT come from here. Its colours are the values that were accepted by eye in the 1.1 baseline, written literally in the rule itself. Three rounds of deriving them from a shared cite ink - 15/6, 35/7, 48/7 with the rule at 35 then 25 percent - each measured correctly and none of them looked like the accepted version. A colour that was approved as a literal is stored as a literal. There is no cite ink token any more. It fed nothing but the blockquote, and an abstraction kept only for the symmetry of having one is dead weight. ==================================================================== */ [data-message-author-role="assistant"] .markdown { --dg-surface-soft: color-mix(in srgb, currentColor 5%, transparent); --dg-edge-soft: color-mix(in srgb, currentColor 18%, transparent); } /* ===================== 1. verified spacing ======================== Unchanged since 1.2.0. Do not retune. ==================================================================== */ [data-message-author-role="assistant"] .markdown p { margin: 0.45em 0; line-height: 1.65; } [data-message-author-role="assistant"] .markdown h2 { margin: 1.15em 0 0.45em; font-size: 1.15em; } [data-message-author-role="assistant"] .markdown h3 { margin: 0.9em 0 0.35em; font-size: 1.05em; } [data-message-author-role="assistant"] .markdown :is(ul, ol) { margin-top: 0.35em; margin-bottom: 0.45em; } [data-message-author-role="assistant"] .markdown li { margin: 0.12em 0; } /* ======================== 2. inline code ========================== `:not(pre) > code` keeps this off fenced blocks, which stay exactly as ChatGPT ships them: background, colours, font size, highlighting. ==================================================================== */ [data-message-author-role="assistant"] .markdown :not(pre) > code { background: var(--dg-surface-soft); padding: 0.08em 0.28em; border-radius: 4px; border: none; box-shadow: none; } /* ========================= 3. blockquote ========================== Reset first, then one structure. !important is used here because the symptom is ChatGPT's own declarations winning the cascade; the reset has to land regardless of what specificity they use. Covers every source that can draw a second line on the blockquote itself: any border side (physical or logical), an inset box-shadow, a background image, and generated content. A bar drawn by a *child* element is the one case this cannot reach - see docs/selectors.md. ==================================================================== */ [data-message-author-role="assistant"] .markdown blockquote { border: 0 !important; border-inline: 0 !important; border-block: 0 !important; box-shadow: none !important; background-image: none !important; text-indent: 0 !important; border-left: 3px solid rgba(100, 116, 139, 0.85) !important; border-radius: 6px !important; background-color: rgba(100, 116, 139, 0.09) !important; padding: 0.55em 0.85em !important; margin: 0.65em 0 !important; color: inherit !important; } /* generated quote marks and pseudo-element bars */ [data-message-author-role="assistant"] .markdown blockquote::before, [data-message-author-role="assistant"] .markdown blockquote::after, [data-message-author-role="assistant"] .markdown blockquote p::before, [data-message-author-role="assistant"] .markdown blockquote p::after { content: none !important; } /* horizontal offset: no inherited indent on the text inside */ [data-message-author-role="assistant"] .markdown blockquote p { margin-inline: 0 !important; } /* vertical offset: the block's own padding is the only top/bottom gap */ [data-message-author-role="assistant"] .markdown blockquote > :first-child { margin-top: 0 !important; } [data-message-author-role="assistant"] .markdown blockquote > :last-child { margin-bottom: 0 !important; } /* ========================== 4. emphasis =========================== Italic alone is too weak to separate at this line-height. 500 is a half step, not a second bold. Bold-italic must stay bold. ==================================================================== */ [data-message-author-role="assistant"] .markdown :is(em, i) { font-weight: 500; } [data-message-author-role="assistant"] .markdown :is(strong, b) :is(em, i) { font-weight: inherit; } /* ======================= 5. horizontal rule ======================= A hairline at the low end of the edge range, not a chapter break. ==================================================================== */ [data-message-author-role="assistant"] .markdown hr { border: 0; border-top: 1px solid var(--dg-edge-soft); margin: 1.1em 0; } /* ======================= 6. content width ========================= Two rules, both needed, neither a blanket selector.
is the inheritance root: a semantic landmark element, not a utility class, so it is not a Tailwind-churn hook. Everything that merely inherits the variable - the composer among them - is covered from there. But
is NOT the only declaration site. ChatGPT sets the variable again further down, on the turn wrapper, through a Tailwind arbitrary property:
A declaration on the element itself beats an inherited value, so that 40rem shadowed everything above it and the column measured 560px regardless of the preset. The second rule overrides the variable exactly where it is redeclared, matched by the *variable's own name* inside the class attribute rather than by any styling class, so it survives Tailwind churn and it also catches the container-query variant. This is deliberately not `main *`. Only the nodes that redeclare the variable need overriding; everything else inherits. Four recommended stops plus a free slider: Reading 44rem prose, narrowest column Balanced 54rem default, everyday reading Wide 66rem code blocks and tables Ultra 78rem large screens, reference material Custom 36-90rem in 1rem steps, from the slider Wiring. @preprocessor default compiles every @var into a CSS custom property in a :root block prepended to this section. It does NOT substitute USO-style square-bracket placeholders - those belong to @preprocessor uso. So the variables are read here with plain var(), under the exact names declared above: --dg-width-preset and --dg-width-custom. A USO placeholder in this file is not an error, which is what made it dangerous: the CSS parser treats it as a comment, the declaration ends up empty, an empty custom property is NOT the same as an unset one, so var(name, fallback) does not fall back, min() receives an empty argument, and the whole width feature silently does nothing. That was the state of every release up to 2.0.0. How the two settings combine, with no conditionals available in plain CSS: a preset compiles to its length, and the Custom option compiles to the keyword `initial`. A custom property set to `initial` is the guaranteed-invalid value, so var(--dg-width-preset, var(--dg-width-custom)) falls through to the slider exactly when Custom is selected, and uses the preset otherwise. The slider is ignored by the four presets. The second term is the responsive retreat: a narrow window shrinks the column instead of pinning text to the edges, keeping a 1.5rem gutter. It measures the container, not the window. Up to 2.5.0 it was calc(100vw - 3rem), and 100vw is the whole window including the sidebar - so the clamp allowed a column wider than the space
actually has. Measured in a mock of this layout: a 1440px window with a 260px sidebar leaves main 1143px, while the Ultra clamp still permitted 1248px. That is only inert while every consumer uses the variable as a max-width; anything using it as a width overflows by that difference, and an overflowing thread is exactly the pressure that can squeeze a shrinkable sibling - the sidebar. 100cqi is the inline size of the nearest query container. ChatGPT declares one named `main` (the @w-lg/main: variant in the turn wrapper's class list is proof of it), so the clamp now tracks the real column. Container inline size is definite by containment, so unlike a percentage it can never fall into cyclic sizing inside an auto-sized grid track or a shrink-to-fit box. With no container ancestor at all, cqi falls back to the small viewport - the old behaviour, not a broken one. Option labels deliberately carry no rem numbers. Stylus stores a select choice by its label, so a label that names its own value would reset every user's saved choice the moment the value is tuned. ==================================================================== */ main, main [class*="--thread-content-max-width:"] { --thread-content-max-width: min(var(--dg-width-preset, var(--dg-width-custom)), calc(100cqi - 3rem)) !important; } }