jockles.com

All the Fannish things I do.

Megaskin by Jockles

← Previous Chapter · Chapter Index · Next Chapter →

Chapter 3: The Core CSS

Thanks to La_Temperanza, whose How To Change Text on AO3 When the Cursor is Hovering Over It (Or Clicked on Mobile) the translations are built on. Copied and adapted from it: swapping the text on hover or tap, the brackets for readers with Creator's Style off, and the link that makes tapping work on phones.

3.1 Chapter Contents

This chapter goes through the CSS behind the last one: the chat box, scrolling, round pictures, hidden text, translations, character profiles and the ten colours every app shares. It's the part of the skin that every module needs, so whichever apps you use, the core goes in first.

You don't need to understand any of this to use the skin. It's here for when you want to change something - a size, a colour, a profile - and want to know which bit to touch without breaking anything else.

3.2 Getting the CSS

The core CSS is here: jockles.com/fic/megaskin/css/core.css. Open it, select everything, and copy it into your work skin before any of the modules. Each module chapter has its own link like this one, and the very last chapter has the whole megaskin in one file.

Where to paste it on AO3

Go to your Dashboard, then Skins, then My Work Skins, then Create Work Skin. Give it a title, paste the CSS into the CSS box and press Submit. If AO3 complains about a line, it'll tell you which one - check you've copied the whole file, from the first /* to the last */.

To add a module later, edit the skin and paste the module's CSS underneath what's already there.

3.3 How the CSS is Laid Out

Every rule in the skin starts with #workskin. AO3 insists on it: it's how AO3 keeps a work skin inside your fic, so it can't touch the rest of the page. If you write your own rules, start them with #workskin too, or AO3 will throw them out.

AO3 also doesn't allow CSS variables or @import, so every colour is written out in full wherever it's used. The exception is the ten colours for pictures and names, which every app shares - they're all together in the core, so there's only one place to change them (see The Ten Colours, below).

The skin is split into blocks, and each block starts and ends with a line like this:

/* >>>>>>>>>> CORE (start) >>>>>>>>>> */
...
/* <<<<<<<<<< CORE (end) <<<<<<<<<< */

The core comes first, then one block per app, in the same order as the chapters. Each app's block only needs the core, so you can keep the core plus the apps you use and delete the rest, from its start line to its end line.

3.4 The Chat Box

Every chat apart from Discord sits in a mob box, and this is where it gets its size:

#workskin .mob {
  width: 380px;
  max-width: 100%;
  margin: 1.2em auto;
  padding: 0;
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: 0.92em;
  line-height: 1.35;
  text-align: left;
  box-sizing: border-box;
  overflow: hidden;
}

#workskin .mob p { margin: 0; padding: 0; text-indent: 0; }
  • width: 380px is the width of a phone, and max-width: 100% lets it shrink on a screen narrower than that, so it never runs off the side.
  • margin: 1.2em auto leaves a gap above and below, and auto centres it on the page.
  • The font is the plain sans-serif that phones use, rather than whatever the reader has AO3 set to.
  • The second rule takes away the gaps AO3 puts around every paragraph, and the indent some site skins add to the first line. Every message is a <p>, so without it the chats would be full of holes.

The header, the body and the centred system lines come next:

#workskin .mob-head { padding: 0.6em 0.9em; text-align: center; font-weight: bold; }

#workskin .mob-body { padding: 0.7em 0.7em 0.5em 0.7em; }

#workskin .mob-sys {
  display: block;
  text-align: center;
  font-size: 0.78em;
  margin: 0.6em 0;
  opacity: 0.75;
}

These only set the shape. The colours are in each app's module, which is how the same mob-head can be grey in iMessage and green in WhatsApp.

Changing the size of every chat
  • Wider or narrower: change 380px. Every module is designed for 380, so anything much over 420 starts to look less like a phone.
  • Bigger or smaller text: change font-size: 0.92em. Everything inside a chat is sized in em, so bubbles, pictures and gaps all grow and shrink with it.
  • A different font: change font-family. Put the font you want first, and keep the others after it for readers who don't have it.

3.5 Using the Reader's Own Font

Each app sets its own font, so the chats look like the real thing - the core's is the plain phone font above, and some apps have their own (Gmail uses Roboto, Discord uses Whitney, Tumblr's quote posts use Georgia, and so on). That's the default, and it's what the skin does as it comes.

Some readers set their own font on their device or in their AO3 site skin, often for accessibility - a dyslexia-friendly font, for example. The app fonts override theirs inside the chats. If you'd rather your chats used the reader's font instead, this isn't in the main CSS, but you can add it yourself. Paste it at the very end of your work skin:

#workskin *,
#workskin *::before,
#workskin *::after {
  font-family: inherit !important;
}
  • * means "everything", so this catches every part of every app, plus the bits the CSS adds itself with ::before and ::after.
  • inherit means "use whatever font the thing around you has", so everything ends up in the reader's font, all the way down.
  • !important makes it win over every app's own font, wherever they are in the skin.

The catch: the skin is spaced and sized for the default fonts. With some fonts - especially wide ones, or ones with tall or deep letters - names might not fit on one line, text can get cut off with "..." sooner, initials can sit off-centre in their circles, and some gaps and boxes might look a bit wonky. Check your chats with a couple of different fonts before you post.

Keeping code and chat posts in a typewriter font

The rule above also changes the typewriter-style font in Discord code blocks and Tumblr chat posts. To keep those, add this underneath it:

#workskin .dc .dc-code,
#workskin .tb .tb-chat {
  font-family: monospace !important;
}

Because it comes after the first rule and is more specific, it wins for just those two.

3.6 Phone Width or Full Width

#workskin .mob.wide {
  width: auto;
  max-width: none;
}

wide undoes the phone width, so the chat fills whatever it's sitting in. .mob.wide with no space means "a box that has both mob and wide", so it only touches chats you've added wide to.

3.7 Scrolling

#workskin .mob-body { max-height: 580px; overflow-y: auto; overflow-x: hidden; }

#workskin .mob.no-scroll {
  max-height: none;
  overflow-y: visible;
}

#workskin .mob.no-scroll .mail-body,
#workskin .mob.no-scroll .mob-body {
  max-height: none;
  overflow-y: visible;
}
  • max-height: 580px is about the height of a phone screen. A chat shorter than that isn't affected at all.
  • overflow-y: auto adds a scroll bar only when it's needed, and overflow-x: hidden stops a sideways one appearing.
  • Because it's the mob-body that scrolls and not the whole box, the header and message bar stay where they are. Posts with no mob-body, like tweets, get their scrolling in their own module.
  • no-scroll puts it all back to normal, so the chat grows as tall as it needs to.

To make every chat taller or shorter before it scrolls, change 580px.

The slim scroll bar
#workskin .mob-body::-webkit-scrollbar {
  width: 6px;
}

#workskin .mob-body::-webkit-scrollbar-thumb {
  background: rgba(128, 128, 128, 0.5);
  border-radius: 6px;
}

This makes the scroll bar thin and grey, like a phone's. It only works in Chrome, Edge and Safari - Firefox keeps its own scroll bar, which is fine, it just looks a little chunkier. The grey is see-through (the 0.5), so it works on light and dark chats alike.

3.8 Round Pictures

Every profile picture, in every app, is a pfp span inside that app's circle. The circle decides the size, and pfp does the cropping:

#workskin .pfp {
  display: block;
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  border-radius: 50%;
  background-size: cover;
  background-position: center;
}

#workskin .pfp img {
  position: absolute;
  top: 0;
  left: 0;
  display: block;
  width: 100%;
  height: auto;
  max-width: none;
  max-height: none;
  margin: 0;
}

#workskin .pfp img.land {
  right: -9999px;
  left: -9999px;
  width: auto;
  height: 100%;
  margin: 0 auto;
}
  • pfp fills its circle (100% each way), rounds itself off (border-radius: 50%) and hides anything that pokes out (overflow: hidden). That's the crop.
  • The picture inside is pinned to the top left and made as wide as the circle. A tall picture then pokes out of the bottom and gets cut off, so the top of the photo - usually the face - stays in view.
  • A wide picture made as wide as the circle wouldn't be tall enough to fill it, which is what land is for. It makes the picture as tall as the circle instead. The -9999px on both sides with margin: 0 auto is a trick for centring something wider than its box: it gives the picture an enormous space to sit in, and auto puts it in the middle.
  • background-size: cover is for profiles, which set their picture as a background instead of a picture tag. cover crops a background to fill the circle by itself, whatever its shape, which is why profile pictures never need land.

A circle with no picture in it shows initials on a plain colour instead. The colours for that, colour1 to colour10, are near the end of the core - see The Ten Colours, below.

3.9 Hidden Text

#workskin .hide.hide.hide.hide { display: none; }

That's the whole thing. display: none takes the text away while the skin is on, and when a reader turns Creator's Style off, the rule goes with the rest of the skin, so the text comes back.

The class is written four times on purpose. When two rules disagree, the browser goes with the more specific one, and every extra class makes a rule more specific. Lots of rules in the modules set display on spans and paragraphs, and some of them could otherwise win and make hidden text show up in the middle of a chat. Four times beats all of them.

3.10 Video Descriptions

#workskin .vid-desc {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

This doesn't hide the description, it just moves it a long way off the left edge of the screen and shrinks it to a single dot. Sighted readers never see it, but screen readers still read it out, because as far as they're concerned it's still on the page. display: none, like hide uses, would hide it from screen readers too.

3.11 Typing Line and Deleted Text

#workskin .mob .typing {
  display: block;
  font-size: 0.75em;
  font-style: italic;
  opacity: 0.7;
  margin: 0.3em 0.9em;
}

#workskin .mob .del, #workskin .dc .del { text-decoration: line-through; opacity: 0.6; }

The typing line is only for phone chats (mob), because Discord has its own typing indicator. Deleted text works in Discord too, so its rule names dc as well.

opacity fades the text, so it's lighter than the messages on a light chat and darker on a dark one, without needing a colour for each. Lower the number to fade it more.

3.12 Links That Go Nowhere

#workskin .mob .fake-link,
#workskin .mob a:not(.tl) {
  text-decoration: underline;
}

#workskin .mob .fake-link,
#workskin .mob a:not(.tl) { cursor: pointer; }

A fake-link and a real link get the same underline and the same pointing-hand cursor, so readers can't tell them apart. a:not(.tl) means "any link that isn't a translation", because translations are links too and need to look different. The colours are in each module, under its Links heading.

3.13 Hover and Tap Translations

This is the cleverest bit of the core, and it's based on La_Temperanza's tutorial (see the credits in chapter 1). Here's the HTML again, to have it in front of you:

<a class="tl" href="#t1"><span class="orig">Diolch yn fawr</span><span class="tltn"><span class="hide"> (</span>Thank you very much<span class="hide">)</span></span></a>

First, the translation link stops looking like a link:

#workskin a.tl,
#workskin a.tl:link,
#workskin a.tl:visited,
#workskin a.tl:hover,
#workskin a.tl:focus,
#workskin a.tl:active {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px dotted; /* delete this line to hide the dotted line on every translation (for just one, use the egg class below) */
}

color: inherit makes it the same colour as the text around it, so it fits into any bubble in any theme, and the dotted line underneath is the only clue it's there. The rule lists the link in every state - visited, hovered and so on - because browsers have their own colours for each, and they all need switching off.

The one line to delete: if you want every translation in your fic to have no dotted line, like an easter egg, delete border-bottom: 1px dotted;, comment and all. Nothing else needs changing.

Next, the translation is hidden until it's needed:

#workskin a.tl span.tltn {
  display: none;
  margin: 0;
  padding: 0;
}

#workskin a.tl span.hide { display: none; }

Then, on hover or tap, the two swap over:

#workskin a.tl:hover,
#workskin a.tl:focus {
  display: inline-block;
}

#workskin a.tl:hover span.orig,
#workskin a.tl:focus span.orig {
  display: block;
  height: 0;
  overflow: hidden;
  visibility: hidden;
}

#workskin a.tl:hover span.tltn,
#workskin a.tl:focus span.tltn {
  display: block;
}
  • :hover is the mouse being over the words, and :focus is a tap on a phone. Tapping a link gives it focus, which is why translations are links, and the #t1 means the tap doesn't take the reader anywhere.
  • The original isn't removed, just made invisible and flattened to no height. It still takes up its full width, so the link never gets narrower than it was. If it did, a short translation could slide out from under the mouse, the original would come back, slide under the mouse again, and the words would flicker.
  • inline-block lets the link hold the two pieces stacked on top of each other while still sitting in the middle of a sentence.

The cursor is set to help - the arrow with a question mark - so computer readers get a hint that there's something there.

Easter eggs
#workskin a.tl.egg,
#workskin a.tl.egg:link,
#workskin a.tl.egg:visited,
#workskin a.tl.egg:hover,
#workskin a.tl.egg:focus,
#workskin a.tl.egg:active {
  border-bottom: 0;
  outline: 0;
}

#workskin a.tl.egg,
#workskin a.tl.egg:link,
#workskin a.tl.egg:visited,
#workskin a.tl.egg:hover,
#workskin a.tl.egg:focus,
#workskin a.tl.egg:active { cursor: text; }

egg takes away the dotted line, and the box browsers draw around a tapped link (outline: 0). The cursor becomes the ordinary text cursor, so not even the mouse gives it away.

3.14 Character Profiles, Line by Line

A profile is a block of CSS for one character. The trick behind it is that CSS can't put text on a page by itself, but it can add text before or after something with ::before and ::after. So <span class="handle pfp-luca"></span> is an empty span, and the profile fills it in. Because the words come from the CSS, they disappear when the skin is off, which is why every example has a hide copy typed inside.

3.14.1 A Simple Profile: Stan

Stan's is the simplest profile in the skin. It only has the five lines every profile needs:

/* PROFILE: Stan */
#workskin .pfp.pfp-stan { background-color: #c06a1f; }
#workskin .wa .who.pfp-stan, #workskin .dc .dc-name.pfp-stan { color: #d2782a; }
#workskin .name.pfp-stan::before { content: "Stan"; }
#workskin .handle.pfp-stan::after { content: "PuckStan"; }
#workskin .email.pfp-stan::after { content: "Puckstan@gmail.com"; }

Here's what each line does:

  1. The picture. Any pfp span that also has pfp-stan gets this. Stan doesn't have a photo, so his is just a plain orange circle (background-color). For a real picture, swap it for background-image: url(https://example.com/stan.jpg);, like Luca's below.
  2. The name colour. WhatsApp group names and Discord usernames are coloured, so this is Stan's colour in both. It does the same job as colour1 to colour10, just for one character, so you don't need a colour class as well. Pick a colour you can read on white and on black, because readers will see it in both themes.
  3. The name. <span class="name pfp-stan"></span> is an empty span, and ::before with content fills it in with "Stan".
  4. The handle. The same idea for <span class="handle pfp-stan"></span>, but with ::after. Leave the @ off - Twitter/X adds it for you (there's more on why in line 8 of Luca's, below).
  5. The email address. The same again, for <span class="email pfp-stan"></span>.

That's a whole profile. The comment line at the top (/* PROFILE: Stan */) doesn't do anything - it's just a label, so you can find each character's block.

3.14.2 Every Option: Luca

Luca's profile uses every option, so here it is a line at a time. Only the lines marked every profile need to be in yours - they're the same five as Stan's.

1. Picture (every profile)

#workskin .pfp.pfp-luca { background-image: url(https://example.com/luca.jpg); }

Any pfp span that also has pfp-luca gets this picture as its background. background-size: cover, from Round Pictures above, crops it to the circle. For a plain colour instead, use background-color: #c06a1f;, like Stan's.

2. A different picture on one app

#workskin .tw .pfp.pfp-luca, #workskin .tw-reply .pfp.pfp-luca { background-image: url(https://example.com/luca-cat.jpg); }

The space before .pfp means "inside". So this is "Luca's picture, inside a tweet or a reply". It's more specific than line 1, so it wins on Twitter/X, and line 1 still covers every other app. Each app has its own module classes - tw and tw-reply for tweets, ig and igdm for Instagram - so give it all of them.

3. Name colour (every profile)

#workskin .wa .who.pfp-luca, #workskin .dc .dc-name.pfp-luca { color: #1f9e89; }

WhatsApp group names and Discord usernames are coloured, so this sets Luca's colour in both. Pick one that you can read on white and on black, because readers will see it in both themes.

4. A different name colour for one app

#workskin .dc .dc-name.pfp-luca { color: #e8734a; }

Like line 2, a line for one app comes after the general line and wins there.

5. Name (every profile)

#workskin .name.pfp-luca::before { content: "Luca"; }

content is the text that gets added, in quotes.

6. A different name for one app

#workskin .tw .name.pfp-luca::before, #workskin .tw-reply .name.pfp-luca::before, #workskin .twdm .name.pfp-luca::before { content: "Luca | cat dad"; }

7. Saved as

#workskin .mob.pov-shane .name.pfp-luca::before { content: "Luca (cat pictures)"; }
#workskin .mob.pov-ilya .name.pfp-luca::before { content: "Cat Guy"; }

This is "Luca's name, inside a chat on Shane's phone". pov-shane goes on the chat's container, so every name in that chat changes to what Shane has saved. One line per phone, and it goes in the profile of the person being named, so everything about how Luca shows up is in Luca's block.

8. Handle (every profile)

#workskin .handle.pfp-luca::after { content: "luca"; }

Handles use ::after instead of ::before, because Twitter/X needs the ::before for its @:

#workskin .tw span.handle::before,
#workskin .tw-reply span.handle::before,
#workskin .twdm span.handle::before { content: "@"; }

So on Twitter/X, a handle gets an @ in front and the name after it, and everywhere else it's just the name. That's why you leave the @ off.

9. A different handle for one app

#workskin .ig .handle.pfp-luca::after, #workskin .igdm .handle.pfp-luca::after { content: "luca.grams"; }

10. Email address

#workskin .email.pfp-luca::after { content: "luca@example.com"; }

11. A different email address in Gmail or the email window

#workskin .mail .email.pfp-luca::after { content: "luca@work.example.com"; }

.mail is the generic email window, and .gm is Gmail.

12. Their phone's wallpaper

#workskin .mob.wa.pov-luca { background: linear-gradient(#fdf1d6, #f6d5c2); }

A WhatsApp chat with pov-luca on it gets this background. A colour, a gradient (one colour fading into another, like this) or url(...) with a picture all work.

Why names, handles and emails look like the text around them
#workskin .mob span.handle.handle.handle.handle,
#workskin .dc span.handle.handle.handle.handle,
#workskin .mob span.name.name.name.name,
#workskin .dc span.name.name.name.name,
#workskin .mob span.email.email.email.email {
  display: inline;
  float: none;
  margin: 0;
  padding: 0;
  font-size: 1em;
  font-weight: inherit;
  color: inherit;
  opacity: 1;
}

A profile span can end up inside almost anything - a bold name line, a grey subline, a link - and some of those parts have rules for every span inside them. This rule wipes all of that off, so a handle takes on the look of wherever it's sitting. The classes are repeated for the same reason as hide: so this rule beats everything else.

3.15 Making Your Own Profile

Copy Stan's block from above and change stan to your character's name everywhere in it:

/* PROFILE: Stan */
#workskin .pfp.pfp-stan { background-color: #c06a1f; }
#workskin .wa .who.pfp-stan, #workskin .dc .dc-name.pfp-stan { color: #d2782a; }
#workskin .name.pfp-stan::before { content: "Stan"; }
#workskin .handle.pfp-stan::after { content: "PuckStan"; }
#workskin .email.pfp-stan::after { content: "Puckstan@gmail.com"; }
Things that trip people up
  • A profile name can't have spaces in it. Use a hyphen: pfp-shane-work.
  • Keep the quotes around names, handles and emails, and keep the semicolon before the }. A missing quote is the most common reason AO3 rejects a skin.
  • If a name has a quote mark or apostrophe in it, like Cat Guy's Cat, that's fine inside double quotes. A double quote inside needs a backslash in front: "The \"Captain\"".
  • Picture links need to end in .jpg, .png or .gif. AO3 won't accept some links in CSS, even if they work in your HTML.
  • The profiles that come with the skin are only examples. Delete the ones you don't need - they don't do anything unless you use them, but they make the skin longer.

3.16 The Ten Colours

Every app uses the same ten colours, colour1 to colour10, for its round picture circles and for people's names. They all live at the end of the core, so if you change a colour there, it changes in every app at once. Here's everything for colour1:

/* picture circles (the same in light and dark) */
#workskin .wa .wa-pfp.colour1,
#workskin .tw .tw-pfp.colour1,
#workskin .tw-reply .tw-pfp.colour1,
#workskin .tb .tb-pfp.colour1,
#workskin .tb .tb-ask-pfp.colour1,
#workskin .gm .gm-pfp.colour1,
#workskin .dc .dc-name.colour1::before { background-color: #b5733a; }

/* names */
#workskin .wa .who.colour1,
#workskin .wa .wa-quote.colour1 b,
#workskin .dc .colour1 { color: #b8651f; }

#workskin .wa .wa-quote.colour1 { border-left-color: #b8651f; }

/* names in the dark theme */
#workskin .wa.theme-dark .who.colour1,
#workskin .wa.theme-dark .wa-quote.colour1 b,
#workskin .dc.theme-dark .colour1 { color: #f0a25c; }

#workskin .wa.theme-dark .wa-quote.colour1 { border-left-color: #f0a25c; }
  • The first rule is the picture circles. They're the same in light and dark, so there's only one version.
  • The second is names - WhatsApp's group names, the name in a WhatsApp quoted reply, and Discord names. A name colour is a little lighter or brighter than the circle with the same number, because coloured text needs more contrast than a coloured circle to be easy to read. In a WhatsApp quoted reply, the line down the side is the name colour too.
  • The dark theme has a third set, lighter again, for reading on a dark background. The same dark rules are copied for theme-auto inside @media (prefers-color-scheme: dark), like every other theme rule.
  • One extra rule lightens Discord's colour2 circle in the dark theme, because the original dark grey disappears into Discord's dark background.

Every app's class names are already in these rules. Each one lists the circle or name class for every app (wa-pfp for WhatsApp, tw-pfp for Twitter/X, dc-name for Discord, and so on), so the colours work whichever apps you use. If you've only kept some of the apps, leave the lists as they are: a class name for an app you're not using just doesn't match anything in your fic, so it doesn't change or break anything.

Changing one of the ten colours

Search the core for colour1 (or whichever number). You'll find the circle rule, the name rule and the quote line rule, then the same name and quote line rules again under the dark theme and the auto theme, with the lighter shade. Change the colour codes in each.

Pick a light-mode name colour you can read on white, and a dark-mode one you can read on near-black. If you've used profiles instead, the name colour is in each character's profile, and none of this applies.

If you ever add your own app, add its circle and name classes to these lists, and it'll get the same ten colours.

3.17 Themes

Apart from the ten colours above, the core doesn't hold any colours, so there's nothing else about themes in it. Each module has its own dark theme and auto theme at the bottom of its block, and they work the same way everywhere:

  • The module's normal rules are its light theme, so a chat with theme-light, or with no theme class at all, is light.
  • theme-dark has its own rules, which swap the colours over.
  • theme-auto has a copy of the dark rules wrapped in @media (prefers-color-scheme: dark). That asks the reader's device whether it's in dark mode, and only uses the dark colours if it is.

AO3 doesn't let a work skin see which AO3 site skin the reader has picked, so theme-auto goes by their device instead. The next chapter shows what that looks like in practice, with the iMessage CSS.

← Previous Chapter · Chapter Index · Next Chapter →